@hraza01/skyhook 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraza01/skyhook",
3
- "version": "1.0.0",
3
+ "version": "1.1.2",
4
4
  "description": "Interactive CLI for Cloud Composer DAG deployment",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -28,6 +28,8 @@
28
28
  "@clack/prompts": "^1.0.0",
29
29
  "chalk": "^5.6.2",
30
30
  "execa": "^9.6.1",
31
+ "figlet": "^1.10.0",
32
+ "ora": "^9.3.0",
31
33
  "terminal-link": "^5.0.0"
32
34
  },
33
35
  "engines": {
@@ -20,8 +20,20 @@ fi
20
20
  echo "Syncing $SOURCE_PATH to $DEST_PATH..."
21
21
 
22
22
  # Calculate file count (informational)
23
- FILE_COUNT=$(find "$SOURCE_PATH" -type f -not -path '*/.git/*' -not -path '*/__pycache__/*' | wc -l | tr -d ' ')
23
+ FILE_COUNT=$(find "$SOURCE_PATH" -type f \
24
+ -not -path '*/.git/*' \
25
+ -not -path '*/__pycache__/*' \
26
+ -not -path '*/tests/*' \
27
+ -not -path '*/.github/*' \
28
+ -not -name 'pyproject.toml' \
29
+ -not -name 'README.md' \
30
+ -not -name 'Makefile' \
31
+ -not -name '.gitignore' \
32
+ -not -name '.pre-commit-config.yaml' \
33
+ | wc -l | tr -d ' ')
24
34
  echo "Uploading $FILE_COUNT files..."
25
35
 
26
36
  # Perform Sync
27
- gsutil -m rsync -r -x "\.git/.*|__pycache__/.*" "$SOURCE_PATH" "$DEST_PATH"
37
+ # Excludes: .git, __pycache__, tests, .github, and specific project files
38
+ EXCLUDE_REGEX="\.git/.*|__pycache__/.*|tests/.*|\.github/.*|pyproject\.toml$|README\.md$|Makefile$|\.gitignore$|\.pre-commit-config\.yaml$"
39
+ gsutil -m rsync -r -x "$EXCLUDE_REGEX" "$SOURCE_PATH" "$DEST_PATH"
package/src/cli.js CHANGED
@@ -1,11 +1,24 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import { fileURLToPath } from "url"
1
4
  import chalk from "chalk"
2
- import { intro, spinner } from "@clack/prompts"
5
+ import { intro } from "@clack/prompts"
6
+ import ora from "ora"
7
+ import terminalLink from "terminal-link"
8
+ import figlet from "figlet"
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = path.dirname(__filename)
12
+ const packageJson = JSON.parse(
13
+ fs.readFileSync(path.join(__dirname, "../package.json"), "utf-8"),
14
+ )
15
+ const { version } = packageJson
3
16
 
4
17
  /**
5
18
  * Display version information
6
19
  */
7
20
  export function showVersionInfo() {
8
- console.log("skyhook v1.0.0")
21
+ console.log(`skyhook v${version}`)
9
22
  process.exit(0)
10
23
  }
11
24
 
@@ -13,6 +26,12 @@ export function showVersionInfo() {
13
26
  * Display help information
14
27
  */
15
28
  export function showHelpInfo() {
29
+ const gitLink = terminalLink(
30
+ chalk.cyan.underline("@hraza01/skyhook"),
31
+ "https://github.com/hraza01/skyhook",
32
+ )
33
+
34
+ console.clear()
16
35
  console.log(
17
36
  chalk.cyan.bold("\nSkyhook - Cloud Composer DAG Deployment Utility\n"),
18
37
  )
@@ -20,13 +39,16 @@ export function showHelpInfo() {
20
39
  console.log(" skyhook [options] [path]\n")
21
40
  console.log("Options:")
22
41
  console.log(" -h, --help Show this help message")
23
- console.log(" --version Show version number")
24
- console.log(" -v, --verbose Enable verbose logging\n")
42
+ console.log(" -v, --version Show version number")
43
+ console.log(" --verbose Enable verbose logging\n")
25
44
  console.log("Environment Variables (Required):")
26
- console.log(" GCS_BUCKET_NAME Your Composer GCS bucket name")
27
- console.log(" COMPOSER_URL_BASE Your Composer webserver base URL\n")
28
- console.log("For more information, visit:")
29
- console.log(" https://github.com/hraza01/skyhook\n")
45
+ console.log(
46
+ ` ${chalk.red("GCS_BUCKET_NAME")} Your Composer GCS bucket name`,
47
+ )
48
+ console.log(
49
+ ` ${chalk.red("COMPOSER_URL_BASE")} Your Composer webserver base URL\n`,
50
+ )
51
+ console.log(`For more information, visit: ${gitLink}`)
30
52
  process.exit(0)
31
53
  }
32
54
 
@@ -34,20 +56,49 @@ export function showHelpInfo() {
34
56
  * Display the intro banner
35
57
  */
36
58
  export function showIntro() {
37
- intro(
38
- chalk.bgCyan(
39
- chalk.black(" Skyhook / Cloud Composer Deployment Utility "),
59
+ console.clear()
60
+ console.log(
61
+ chalk.cyan(
62
+ figlet.textSync("Skyhook", {
63
+ font: "Slant",
64
+ horizontalLayout: "default",
65
+ verticalLayout: "default",
66
+ }),
40
67
  ),
41
68
  )
69
+ console.log("")
70
+ intro(chalk.bgCyan.black(" Cloud Composer DAG Deployment Utility "))
42
71
  }
43
72
 
44
73
  /**
45
- * Create and return a configured spinner with cyan color
74
+ * Create and return a configured spinner using ora
75
+ * Wraps ora to match the interface used by the rest of the app:
76
+ * - start(msg)
77
+ * - stop(msg, code)
78
+ * - message(msg)
46
79
  */
47
80
  export function createSpinner() {
48
- return spinner({
49
- frames: ["", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].map(
50
- (frame) => chalk.cyan(frame),
51
- ),
81
+ const spinner = ora({
82
+ color: "cyan",
83
+ spinner: "dots",
52
84
  })
85
+
86
+ return {
87
+ start(msg) {
88
+ spinner.start(` ${msg}`)
89
+ },
90
+ stop(msg, code = 0) {
91
+ if (code === 0) {
92
+ spinner.succeed(` ${msg}`)
93
+ } else {
94
+ spinner.fail(` ${msg}`)
95
+ }
96
+ },
97
+ message(msg) {
98
+ spinner.text = ` ${msg}`
99
+ },
100
+ clear() {
101
+ spinner.stop()
102
+ },
103
+ }
53
104
  }
package/src/config.js CHANGED
@@ -6,15 +6,18 @@ const { values, positionals } = parseArgs({
6
6
  options: {
7
7
  verbose: {
8
8
  type: "boolean",
9
- short: "v",
10
9
  },
11
10
  version: {
12
11
  type: "boolean",
12
+ short: "v",
13
13
  },
14
14
  help: {
15
15
  type: "boolean",
16
16
  short: "h",
17
17
  },
18
+ "no-git": {
19
+ type: "boolean",
20
+ },
18
21
  },
19
22
  allowPositionals: true,
20
23
  })
@@ -22,6 +25,7 @@ const { values, positionals } = parseArgs({
22
25
  export const verbose = values.verbose
23
26
  export const showVersion = values.version
24
27
  export const showHelp = values.help
28
+ export const skipGit = values["no-git"]
25
29
 
26
30
  // Helper to get root dir based on arg or CWD
27
31
  export const ROOT_DIR = positionals[0]
@@ -30,6 +34,9 @@ export const ROOT_DIR = positionals[0]
30
34
  export const DAGS_DIR = path.join(ROOT_DIR, "dags")
31
35
 
32
36
  export function validateEnv() {
37
+ // Skip validation if we are just showing help or version
38
+ if (showVersion || showHelp) return
39
+
33
40
  if (!process.env.GCS_BUCKET_NAME || !process.env.COMPOSER_URL_BASE) {
34
41
  throw new ConfigError(
35
42
  "Missing GCS_BUCKET_NAME or COMPOSER_URL_BASE environment variables.",
@@ -1,32 +1,36 @@
1
1
  import fs from "fs"
2
2
  import path from "path"
3
- import { select, isCancel } from "@clack/prompts"
3
+ import { select, isCancel, log } from "@clack/prompts"
4
4
  import { logger } from "./logger.js"
5
5
  import { UserCancellationError } from "./errors.js"
6
+ import chalk from "chalk"
6
7
 
7
8
  export function scanDags(dagsDir, s) {
8
- logger.info("SCAN", `Scanning directory: ${dagsDir}`)
9
- s.start(`Looking for DAGs in: ${path.relative(process.cwd(), dagsDir)}`)
9
+ s.start("Scanning for DAGs...")
10
10
 
11
11
  if (!fs.existsSync(dagsDir)) {
12
- s.stop(`Directory '${dagsDir}' not found.`, 1)
13
- throw new UserCancellationError("Operation cancelled.")
12
+ s.stop("No DAGs directory found.", 1)
13
+ throw new Error(`DAGs directory not found: ${dagsDir}`)
14
14
  }
15
15
 
16
- const folders = fs.readdirSync(dagsDir).filter((file) => {
16
+ const items = fs.readdirSync(dagsDir)
17
+ const folders = items.filter((item) => {
18
+ const fullPath = path.join(dagsDir, item)
17
19
  return (
18
- fs.statSync(path.join(dagsDir, file)).isDirectory() &&
19
- !file.startsWith(".")
20
+ fs.statSync(fullPath).isDirectory() &&
21
+ !item.startsWith(".") &&
22
+ !item.startsWith("__")
20
23
  )
21
24
  })
22
25
 
23
26
  if (folders.length === 0) {
24
- s.stop(`No folders found in ${dagsDir}`, 1)
25
- throw new UserCancellationError("Operation cancelled.")
27
+ s.stop("No DAG folders found.", 1)
28
+ throw new Error("No DAG folders found in dags/ directory.")
26
29
  }
27
30
 
28
- s.stop(`Found ${folders.length} Airflow DAGs.`)
29
- logger.info("SCAN", `Found ${folders.length} valid DAG folders.`)
31
+ s.clear() // Stop spinner and clear line
32
+ log.success(`Found ${folders.length} Airflow DAGs.`)
33
+
30
34
  return folders
31
35
  }
32
36
 
package/src/deploy.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { execa } from "execa"
2
2
  import chalk from "chalk"
3
3
  import path from "path"
4
- import { cancel, outro } from "@clack/prompts"
4
+ import { cancel, outro, log } from "@clack/prompts"
5
5
  import terminalLink from "terminal-link"
6
6
  import { fileURLToPath } from "url"
7
7
  import { countFiles } from "./utils.js"
@@ -86,7 +86,8 @@ export async function deployDag(selectedFolder, sourcePath, s, verbose) {
86
86
  })
87
87
 
88
88
  await subprocess
89
- s.stop("GCS sync complete.")
89
+ s.clear()
90
+ log.success("GCS sync complete.")
90
91
 
91
92
  // Show Summary
92
93
  const fileCount = countFiles(sourcePath)
@@ -101,43 +102,44 @@ export async function deployDag(selectedFolder, sourcePath, s, verbose) {
101
102
  productionUrl,
102
103
  )
103
104
 
104
- console.log("")
105
- console.log(chalk.green("Deployment Summary"))
106
- console.log("")
105
+ // Using console.log for body items to avoid clack's extra spacing
106
+ // Adding a small indentation to align visually
107
+ const indent = `${chalk.gray("")} `
108
+
109
+ log.success(chalk.bold.bgGreen(" Deployment Summary "))
110
+ console.log(indent)
107
111
 
108
112
  console.log(
109
- `${chalk.dim(pad("Source", labelWidth))} ${chalk.reset(
113
+ `${indent}${chalk.dim(pad("Source", labelWidth))} ${chalk.reset(
110
114
  `dags/${selectedFolder}`,
111
115
  )}`,
112
116
  )
113
117
  console.log(
114
- `${chalk.dim(pad("Destination", labelWidth))} ${chalk.reset(
118
+ `${indent}${chalk.dim(pad("Destination", labelWidth))} ${chalk.reset(
115
119
  `${BUCKET_URL}/${selectedFolder}`,
116
120
  )}`,
117
121
  )
118
122
  console.log(
119
- `${chalk.dim(pad("Files Synced", labelWidth))} ${chalk.reset(
123
+ `${indent}${chalk.dim(pad("Files Synced", labelWidth))} ${chalk.reset(
120
124
  `${fileCount} files`,
121
125
  )}`,
122
126
  )
123
-
124
- console.log("")
125
127
  console.log(
128
+ `${indent}${chalk.dim(pad("Composer URL", labelWidth))} ${link}`,
129
+ )
130
+
131
+ log.info(
126
132
  chalk.white(
127
133
  `${chalk.bold(
128
134
  selectedFolder,
129
135
  )} is now in sync with git + Cloud Composer.`,
130
136
  ),
131
137
  )
132
- console.log("")
133
-
134
- // Print URL in original location but single line
135
- console.log(chalk.dim(pad("Composer URL", labelWidth)) + link)
136
- console.log("")
137
138
 
138
- outro(chalk.green.bold("Deployment Successful!"))
139
+ outro(chalk.green.bold("Deployment Successful."))
139
140
  logger.info("DEPLOY", "Deployment steps completed successfully.")
140
141
  } catch (error) {
142
+ // ... (rest of catch block) ...
141
143
  logger.error("DEPLOY", `Deployment failed: ${error.message}`)
142
144
  s.stop("Deployment Failed ❌", 1)
143
145
  console.log(chalk.red("\nError Logs:"))
@@ -1,38 +1,65 @@
1
1
  import { execa } from "execa"
2
2
  import chalk from "chalk"
3
3
  import { ValidationError } from "./errors.js"
4
+ import { log } from "@clack/prompts"
4
5
 
5
6
  export async function validateGit(sourcePath, s) {
7
+ console.log(chalk.gray("│"))
6
8
  s.start("Validating Git status...")
7
9
 
8
- // A. Check if valid git repo
10
+ // A. Check if it is a connected git repo
9
11
  try {
10
- await execa("git", [
12
+ await execa("git", ["-C", sourcePath, "status"])
13
+ } catch (e) {
14
+ s.stop("Validation Failed", 1)
15
+ throw new ValidationError("Directory is not a Git repository.")
16
+ }
17
+
18
+ // B. Check Branch Name
19
+ let branch
20
+ try {
21
+ const { stdout } = await execa("git", [
11
22
  "-C",
12
23
  sourcePath,
13
24
  "rev-parse",
14
- "--is-inside-work-tree",
25
+ "--abbrev-ref",
26
+ "HEAD",
15
27
  ])
28
+ branch = stdout.trim()
16
29
  } catch (e) {
17
- s.stop("Validation Failed: Not a git repository.", 1)
18
- throw new ValidationError("The DAG folder must be version controlled.")
30
+ // Handle "ambiguous argument 'HEAD'" which happens in a fresh repo with no commits
31
+ if (e.message.includes("ambiguous argument 'HEAD'")) {
32
+ s.stop(
33
+ "Validation Failed: No commits found. Please commit your changes.",
34
+ 1,
35
+ )
36
+ throw new ValidationError("Git repository has no commits.")
37
+ }
38
+ throw e
19
39
  }
20
40
 
21
- // B. Check Branch Name
22
- const { stdout: branch } = await execa("git", [
41
+ if (branch !== "main") {
42
+ s.stop("Validation Failed", 1)
43
+ throw new ValidationError(
44
+ `You are on branch "${branch}". Please switch to "main".`,
45
+ )
46
+ }
47
+
48
+ // C. Check for Uncommitted Changes
49
+ const { stdout: statusOutput } = await execa("git", [
23
50
  "-C",
24
51
  sourcePath,
25
- "rev-parse",
26
- "--abbrev-ref",
27
- "HEAD",
52
+ "status",
53
+ "--porcelain",
28
54
  ])
29
- if (branch.trim() !== "main") {
30
- s.stop(`Validation Failed: You are on branch '${branch.trim()}'.`, 1)
31
- throw new ValidationError("You must be on the 'main' branch to deploy.")
55
+ if (statusOutput.trim() !== "") {
56
+ s.stop("Validation Failed", 1)
57
+ throw new ValidationError(
58
+ "You have uncommitted changes. Please commit or stash them.",
59
+ )
32
60
  }
33
61
 
34
- // C. Check Sync Status
35
- s.message("Checking remote sync status...")
62
+ // D. Check Sync Status (Pull/Push)
36
63
  await execa("git", ["-C", sourcePath, "fetch", "origin", "main"])
37
64
 
38
65
  const { stdout: localHash } = await execa("git", [
@@ -49,7 +76,8 @@ export async function validateGit(sourcePath, s) {
49
76
  ])
50
77
 
51
78
  if (localHash.trim() !== remoteHash.trim()) {
52
- s.stop("Validation Failed: Branch is out of sync with origin/main.", 1)
79
+ s.stop()
80
+ log.error("Validation Failed: Branch is out of sync with origin/main.")
53
81
 
54
82
  // Optional: Show ahead/behind info
55
83
  try {
@@ -71,5 +99,6 @@ export async function validateGit(sourcePath, s) {
71
99
  throw new ValidationError("Please pull/push changes before deploying.")
72
100
  }
73
101
 
74
- s.stop("Git validation passed (main branch, in sync).")
102
+ s.clear()
103
+ log.success("Git validation passed (main branch, in sync).")
75
104
  }
package/src/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import chalk from "chalk"
4
- import { cancel } from "@clack/prompts"
4
+ import { cancel, log } from "@clack/prompts"
5
5
  import {
6
6
  validateEnv,
7
7
  DAGS_DIR,
8
8
  verbose,
9
9
  showVersion,
10
10
  showHelp,
11
+ skipGit,
11
12
  } from "./config.js"
12
13
  import {
13
14
  showVersionInfo,
@@ -24,7 +25,6 @@ import path from "path"
24
25
 
25
26
  async function main() {
26
27
  try {
27
- console.clear()
28
28
  initLogger(verbose)
29
29
  logger.info("INIT", "Skyhook started")
30
30
 
@@ -37,25 +37,28 @@ async function main() {
37
37
  validateEnv()
38
38
  showIntro()
39
39
 
40
+ log.info("Looking for DAGs in:")
41
+ console.log(`${chalk.gray("│")} ${chalk.dim(DAGS_DIR)}`)
42
+ log.warn(chalk.dim("Press Ctrl+C to exit at any time."))
43
+
40
44
  const s = createSpinner()
41
45
 
42
- // 1. Scan
43
46
  const folders = scanDags(DAGS_DIR, s)
44
47
 
45
- // 2. Select
46
48
  const selectedFolder = await selectDag(folders)
47
49
  const sourcePath = path.join(DAGS_DIR, selectedFolder)
48
50
 
49
- // 3. Validate
50
- await validateGit(sourcePath, s)
51
+ if (skipGit) {
52
+ logger.info("GIT", "Skipping Git validation (--no-git)")
53
+ } else {
54
+ await validateGit(sourcePath, s)
55
+ }
51
56
 
52
- // 4. Deploy
53
57
  await deployDag(selectedFolder, sourcePath, s, verbose)
54
58
 
55
- // 5. Post-Deployment Polish
56
59
  const quote = await fetchQuote()
57
60
  if (quote) {
58
- console.log(chalk.italic.dim(`\n${quote}\n`))
61
+ console.log(chalk.italic.dim(`${quote}\n`))
59
62
  }
60
63
  } catch (error) {
61
64
  if (error.name === "UserCancellationError") {
package/src/utils.js CHANGED
@@ -8,11 +8,26 @@ export function countFiles(dir) {
8
8
  file = path.resolve(dir, file)
9
9
  const stat = fs.statSync(file)
10
10
  if (stat && stat.isDirectory()) {
11
- if (!file.includes(".git") && !file.includes("__pycache__")) {
11
+ if (
12
+ !file.includes(".git") &&
13
+ !file.includes("__pycache__") &&
14
+ !file.includes("tests") &&
15
+ !file.includes(".github")
16
+ ) {
12
17
  results += countFiles(file)
13
18
  }
14
19
  } else {
15
- results += 1
20
+ const filename = path.basename(file)
21
+ const ignoredFiles = [
22
+ "pyproject.toml",
23
+ "README.md",
24
+ "Makefile",
25
+ ".gitignore",
26
+ ".pre-commit-config.yaml",
27
+ ]
28
+ if (!ignoredFiles.includes(filename)) {
29
+ results += 1
30
+ }
16
31
  }
17
32
  })
18
33
  return results