@ticatec/omniflow-core 0.1.0 → 0.2.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -54
  3. package/README_CN.md +113 -55
  4. package/dist/index.d.ts +5 -4
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/plugin/PluginContext.d.ts +39 -0
  9. package/dist/plugin/PluginContext.d.ts.map +1 -0
  10. package/dist/plugin/PluginContext.js +8 -0
  11. package/dist/plugin/PluginContext.js.map +1 -0
  12. package/dist/primitives/docker.d.ts.map +1 -1
  13. package/dist/primitives/docker.js +9 -0
  14. package/dist/primitives/docker.js.map +1 -1
  15. package/dist/primitives/git.d.ts +58 -16
  16. package/dist/primitives/git.d.ts.map +1 -1
  17. package/dist/primitives/git.js +91 -33
  18. package/dist/primitives/git.js.map +1 -1
  19. package/dist/primitives/shell.d.ts +4 -0
  20. package/dist/primitives/shell.d.ts.map +1 -1
  21. package/dist/primitives/shell.js +20 -60
  22. package/dist/primitives/shell.js.map +1 -1
  23. package/dist/primitives/ssh.d.ts +8 -5
  24. package/dist/primitives/ssh.d.ts.map +1 -1
  25. package/dist/primitives/ssh.js +49 -14
  26. package/dist/primitives/ssh.js.map +1 -1
  27. package/dist/primitives/subprocess.d.ts +52 -0
  28. package/dist/primitives/subprocess.d.ts.map +1 -0
  29. package/dist/primitives/subprocess.js +353 -0
  30. package/dist/primitives/subprocess.js.map +1 -0
  31. package/dist/toolchain/providers/GradleToolchain.d.ts +1 -1
  32. package/dist/toolchain/providers/GradleToolchain.d.ts.map +1 -1
  33. package/dist/toolchain/providers/GradleToolchain.js +4 -3
  34. package/dist/toolchain/providers/GradleToolchain.js.map +1 -1
  35. package/dist/toolchain/providers/MavenToolchain.d.ts +1 -1
  36. package/dist/toolchain/providers/MavenToolchain.d.ts.map +1 -1
  37. package/dist/toolchain/providers/MavenToolchain.js +3 -3
  38. package/dist/toolchain/providers/MavenToolchain.js.map +1 -1
  39. package/dist/toolchain/providers/NodeToolchain.d.ts +1 -1
  40. package/dist/toolchain/providers/NodeToolchain.d.ts.map +1 -1
  41. package/dist/toolchain/providers/NodeToolchain.js +61 -0
  42. package/dist/toolchain/providers/NodeToolchain.js.map +1 -1
  43. package/dist/toolchain/providers/pom.d.ts.map +1 -1
  44. package/dist/toolchain/providers/pom.js +13 -0
  45. package/dist/toolchain/providers/pom.js.map +1 -1
  46. package/dist/toolchain/registry.d.ts +11 -3
  47. package/dist/toolchain/registry.d.ts.map +1 -1
  48. package/dist/toolchain/registry.js +33 -8
  49. package/dist/toolchain/registry.js.map +1 -1
  50. package/dist/utils/mask.d.ts.map +1 -1
  51. package/dist/utils/mask.js +30 -5
  52. package/dist/utils/mask.js.map +1 -1
  53. package/docs/toolchain-extension.md +301 -0
  54. package/docs/toolchain-extension_CN.md +304 -0
  55. package/package.json +23 -1
  56. package/src/context/index.ts +74 -0
  57. package/src/context/storage.ts +8 -0
  58. package/src/context/types.ts +69 -0
  59. package/src/index.ts +97 -0
  60. package/src/plugin/PluginContext.ts +57 -0
  61. package/src/primitives/docker.ts +164 -0
  62. package/src/primitives/git.ts +172 -0
  63. package/src/primitives/index.ts +4 -0
  64. package/src/primitives/shell.ts +157 -0
  65. package/src/primitives/ssh.ts +249 -0
  66. package/src/primitives/subprocess.ts +389 -0
  67. package/src/toolchain/index.ts +6 -0
  68. package/src/toolchain/providers/GradleToolchain.ts +137 -0
  69. package/src/toolchain/providers/MavenToolchain.ts +64 -0
  70. package/src/toolchain/providers/NodeToolchain.ts +172 -0
  71. package/src/toolchain/providers/pom.ts +145 -0
  72. package/src/toolchain/registry.ts +161 -0
  73. package/src/toolchain/types.ts +40 -0
  74. package/src/utils/mask.ts +73 -0
  75. package/src/utils/template.ts +62 -0
@@ -0,0 +1,74 @@
1
+ import { executionLocalStorage } from './storage.js'
2
+ import type { ExecutionContext, Logger } from './types.js'
3
+
4
+ export * from './types.js'
5
+ export { executionLocalStorage } from './storage.js'
6
+
7
+ /**
8
+ * Return the active ExecutionContext for the current asynchronous call stack.
9
+ *
10
+ * @throws Error if called outside an active execution scope (i.e., not inside runWithContext).
11
+ */
12
+ export function getContext(): ExecutionContext {
13
+ const ctx = executionLocalStorage.getStore()
14
+ if (!ctx) {
15
+ throw new Error(
16
+ 'No active OmniFlow ExecutionContext found in this asynchronous scope. ' +
17
+ 'Ensure this function is executed within runWithContext(ctx, fn).'
18
+ )
19
+ }
20
+ return ctx
21
+ }
22
+
23
+ /**
24
+ * Return the active ExecutionContext if available, or undefined.
25
+ */
26
+ export function tryGetContext(): ExecutionContext | undefined {
27
+ return executionLocalStorage.getStore()
28
+ }
29
+
30
+ /**
31
+ * Check whether an active ExecutionContext is available in the current scope.
32
+ */
33
+ export function hasContext(): boolean {
34
+ return executionLocalStorage.getStore() !== undefined
35
+ }
36
+
37
+ /**
38
+ * Run a function within an ExecutionContext scope. All asynchronous calls
39
+ * initiated within `fn` will have access to `ctx` via `getContext()`.
40
+ */
41
+ export async function runWithContext<T>(
42
+ ctx: ExecutionContext,
43
+ fn: () => Promise<T> | T
44
+ ): Promise<T> {
45
+ return executionLocalStorage.run(ctx, fn)
46
+ }
47
+
48
+ /**
49
+ * Helper to build a fallback or testing ExecutionContext.
50
+ */
51
+ export function createMockContext(overrides: Partial<ExecutionContext> = {}): ExecutionContext {
52
+ const defaultLogger: Logger = {
53
+ info: (msg: string) => console.log(`[INFO] ${msg}`),
54
+ warn: (msg: string) => console.warn(`[WARN] ${msg}`),
55
+ error: (msg: string) => console.error(`[ERROR] ${msg}`),
56
+ debug: (msg: string) => console.debug(`[DEBUG] ${msg}`)
57
+ }
58
+
59
+ return {
60
+ runId: overrides.runId ?? `test-run-${Date.now()}`,
61
+ workspace: overrides.workspace ?? process.cwd(),
62
+ projectRoot: overrides.projectRoot ?? process.cwd(),
63
+ project: overrides.project ?? 'test-project',
64
+ module: overrides.module,
65
+ moduleFolder: overrides.moduleFolder,
66
+ environment: overrides.environment ?? 'test',
67
+ git: overrides.git ?? { branch: 'main', commit: '0000000', dirty: false },
68
+ env: overrides.env ?? {},
69
+ logger: overrides.logger ?? defaultLogger,
70
+ dryRun: overrides.dryRun ?? false,
71
+ sourceMode: overrides.sourceMode ?? 'local',
72
+ logFile: overrides.logFile
73
+ }
74
+ }
@@ -0,0 +1,8 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import type { ExecutionContext } from './types.js'
3
+
4
+ /**
5
+ * Node.js AsyncLocalStorage instance managing the active ExecutionContext
6
+ * across asynchronous function calls.
7
+ */
8
+ export const executionLocalStorage = new AsyncLocalStorage<ExecutionContext>()
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Core execution context and command result contracts for OmniFlow.
3
+ */
4
+
5
+ export interface GitMetadata {
6
+ branch?: string
7
+ commit?: string
8
+ tag?: string
9
+ dirty?: boolean
10
+ }
11
+
12
+ export interface Logger {
13
+ info(msg: string): void
14
+ warn(msg: string): void
15
+ error(msg: string): void
16
+ debug?(msg: string): void
17
+ }
18
+
19
+ export interface Artifact {
20
+ type: string
21
+ ref: string
22
+ name?: string
23
+ digest?: string
24
+ path?: string
25
+ metadata?: Record<string, unknown>
26
+ }
27
+
28
+ export interface CommandResult {
29
+ success: boolean
30
+ message?: string
31
+ artifacts?: Artifact[]
32
+ outputs?: Record<string, unknown>
33
+ error?: Error | string
34
+ }
35
+
36
+ export type SourceMode = 'local' | 'ref' | 'managed'
37
+
38
+ /**
39
+ * Pure read-only execution context representing the active run state.
40
+ * Bound to the current async execution chain via AsyncLocalStorage.
41
+ */
42
+ export interface ExecutionContext {
43
+ /** Unique run identifier */
44
+ runId: string
45
+ /** Physical directory path for workspace */
46
+ workspace: string
47
+ /** Physical project root directory */
48
+ projectRoot: string
49
+ /** Project key/name */
50
+ project: string
51
+ /** Module name currently executing, if applicable */
52
+ module?: string
53
+ /** Relative subdirectory of the active module */
54
+ moduleFolder?: string
55
+ /** Target deployment environment name (test, prod, etc.) */
56
+ environment: string
57
+ /** Git repository state at execution time */
58
+ git: GitMetadata
59
+ /** Resolved and merged environment variables */
60
+ env: Record<string, string>
61
+ /** Structured logger directing output to terminal and run record */
62
+ logger: Logger
63
+ /** Whether the execution is running in dry-run mode */
64
+ dryRun: boolean
65
+ /** Active source resolution mode */
66
+ sourceMode?: SourceMode
67
+ /** Path to the log file receiving all process and command output */
68
+ logFile?: string
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @ticatec/omniflow-core
3
+ *
4
+ * Foundational execution context, primitives, and toolchain SPI
5
+ * for OmniFlow CI/CD orchestrator and its plugins.
6
+ */
7
+
8
+ // Context
9
+ export {
10
+ getContext,
11
+ tryGetContext,
12
+ hasContext,
13
+ runWithContext,
14
+ createMockContext,
15
+ executionLocalStorage,
16
+ type ExecutionContext,
17
+ type CommandResult,
18
+ type Artifact,
19
+ type Logger,
20
+ type GitMetadata,
21
+ type SourceMode
22
+ } from './context/index.js'
23
+
24
+ // Primitives
25
+ export {
26
+ shell,
27
+ run,
28
+ sh,
29
+ output,
30
+ createMaskTransform,
31
+ type RunOptions,
32
+ type ShellOutputResult,
33
+ type TemplateRunner
34
+ } from './primitives/shell.js'
35
+
36
+ export {
37
+ ssh,
38
+ SshClient,
39
+ type SshServerConfig,
40
+ type SshConnectionConfig,
41
+ type SshTarget,
42
+ type SshResult
43
+ } from './primitives/ssh.js'
44
+
45
+ export {
46
+ git,
47
+ type GitCwdOptions,
48
+ type GitFetchOptions,
49
+ type GitResetOptions,
50
+ type GitCheckoutOptions,
51
+ type GitCleanOptions
52
+ } from './primitives/git.js'
53
+
54
+ export {
55
+ docker,
56
+ type DockerBuildOptions,
57
+ type ComposeOptions
58
+ } from './primitives/docker.js'
59
+
60
+ // Toolchains
61
+ export {
62
+ registerToolchain,
63
+ resolveToolchain,
64
+ ToolchainRegistry,
65
+ defaultToolchainRegistry,
66
+ MavenToolchain,
67
+ GradleToolchain,
68
+ NodeToolchain,
69
+ readPom,
70
+ parseXml,
71
+ type ToolchainProvider,
72
+ type DetectionResult,
73
+ type ProjectInfo,
74
+ type ResolveToolchainOptions
75
+ } from './toolchain/index.js'
76
+
77
+ // Utilities
78
+ export {
79
+ maskString,
80
+ maskObject,
81
+ isSecretKey,
82
+ MASK
83
+ } from './utils/mask.js'
84
+
85
+ export {
86
+ templateToArgv
87
+ } from './utils/template.js'
88
+
89
+ // Plugin SPI
90
+ export {
91
+ type PluginContext,
92
+ type PluginRegisterFn,
93
+ type ArtifactRegistryLike,
94
+ type BranchRegistryLike,
95
+ type CommandRegistryLike,
96
+ type ToolchainRegistryLike
97
+ } from './plugin/PluginContext.js'
@@ -0,0 +1,57 @@
1
+ /**
2
+ * OmniFlow Plugin SPI Contracts
3
+ *
4
+ * Provides the context and registration hook for external plugins extending
5
+ * artifact publishers, branch strategies, commands, and toolchains.
6
+ */
7
+
8
+ export interface ArtifactRegistryLike {
9
+ register(publisher: any): void
10
+ }
11
+
12
+ export interface BranchRegistryLike {
13
+ register(strategy: any): void
14
+ }
15
+
16
+ export interface CommandRegistryLike {
17
+ register(command: any): void
18
+ registerFunction?(name: string, fn: any): void
19
+ }
20
+
21
+ export interface ToolchainRegistryLike {
22
+ register(toolchain: any): void
23
+ }
24
+
25
+ /**
26
+ * Standard SPI context passed to every plugin's `register(ctx, options)` hook.
27
+ */
28
+ export interface PluginContext<
29
+ TArtifactRegistry = any,
30
+ TBranchRegistry = any,
31
+ TCommandRegistry = any,
32
+ TToolchainRegistry = any
33
+ > {
34
+ /** Registry for Docker, NPM, Maven, Raw artifact publishers */
35
+ artifactRegistry: TArtifactRegistry
36
+ /** Registry for Git branch promotion and PR/MR drivers */
37
+ branchRegistry: TBranchRegistry
38
+ /** Registry for BaseCommand subclasses or functional recipes */
39
+ commandRegistry: TCommandRegistry
40
+ /** Registry for detection and build toolchains */
41
+ toolchainRegistry: TToolchainRegistry
42
+ /** Runtime environment variables */
43
+ env: Record<string, string | undefined>
44
+ }
45
+
46
+ /**
47
+ * The standard entry function every OmniFlow plugin exports.
48
+ */
49
+ export type PluginRegisterFn<
50
+ TArtifactRegistry = any,
51
+ TBranchRegistry = any,
52
+ TCommandRegistry = any,
53
+ TToolchainRegistry = any
54
+ > = (
55
+ ctx: PluginContext<TArtifactRegistry, TBranchRegistry, TCommandRegistry, TToolchainRegistry>,
56
+ options?: Record<string, unknown>
57
+ ) => Promise<void> | void
@@ -0,0 +1,164 @@
1
+ import path from 'node:path'
2
+ import { shell } from './shell.js'
3
+ import { tryGetContext } from '../context/index.js'
4
+ import type { Artifact } from '../context/types.js'
5
+
6
+ export interface DockerBuildOptions {
7
+ image: string
8
+ dockerfile?: string
9
+ target?: string
10
+ buildArgs?: Record<string, string>
11
+ cwd?: string
12
+ flags?: string[]
13
+ }
14
+
15
+ export interface ComposeOptions {
16
+ files?: string | string[]
17
+ services?: string[]
18
+ detach?: boolean
19
+ build?: boolean
20
+ envFile?: string
21
+ cwd?: string
22
+ flags?: string[]
23
+ }
24
+
25
+ export const docker = {
26
+ /**
27
+ * Build a Docker image.
28
+ */
29
+ async build(options: DockerBuildOptions): Promise<Artifact> {
30
+ if (!options.image || typeof options.image !== 'string' || options.image.trim() === '') {
31
+ throw new Error('Docker build requires a non-empty "image" identifier')
32
+ }
33
+
34
+ const ctx = tryGetContext()
35
+ const cwd = options.cwd ?? (ctx?.moduleFolder ? path.resolve(ctx.workspace, ctx.moduleFolder) : ctx?.workspace ?? process.cwd())
36
+ const dockerfile = options.dockerfile ?? 'Dockerfile'
37
+
38
+ const args: string[] = ['build', '-f', dockerfile, '-t', options.image]
39
+
40
+ if (options.target) {
41
+ args.push('--target', options.target)
42
+ }
43
+
44
+ if (options.buildArgs) {
45
+ for (const [k, v] of Object.entries(options.buildArgs)) {
46
+ args.push('--build-arg', `${k}=${v}`)
47
+ }
48
+ }
49
+
50
+ if (options.flags) {
51
+ args.push(...options.flags)
52
+ }
53
+
54
+ args.push('.')
55
+
56
+ ctx?.logger.info(`Building Docker image: ${options.image} (file: ${dockerfile})`)
57
+ await shell.run({ cwd })`docker ${args}`
58
+
59
+ return {
60
+ type: 'docker-image',
61
+ ref: options.image
62
+ }
63
+ },
64
+
65
+ /**
66
+ * Tag an existing Docker image.
67
+ */
68
+ async tag(sourceImage: string, targetImage: string): Promise<Artifact> {
69
+ const ctx = tryGetContext()
70
+ ctx?.logger.info(`Tagging Docker image: ${sourceImage} -> ${targetImage}`)
71
+ await shell.run`docker tag ${sourceImage} ${targetImage}`
72
+ return {
73
+ type: 'docker-image',
74
+ ref: targetImage
75
+ }
76
+ },
77
+
78
+ /**
79
+ * Push a Docker image to a registry.
80
+ */
81
+ async push(image: string): Promise<Artifact> {
82
+ const ctx = tryGetContext()
83
+ ctx?.logger.info(`Pushing Docker image: ${image}`)
84
+ await shell.run`docker push ${image}`
85
+ return {
86
+ type: 'docker-image',
87
+ ref: image
88
+ }
89
+ },
90
+
91
+ /**
92
+ * Launch services using Docker Compose.
93
+ */
94
+ async composeUp(options: ComposeOptions = {}): Promise<void> {
95
+ const ctx = tryGetContext()
96
+ const cwd = options.cwd ?? (ctx?.moduleFolder ? path.resolve(ctx.workspace, ctx.moduleFolder) : ctx?.workspace ?? process.cwd())
97
+ const args: string[] = ['compose']
98
+
99
+ if (options.files) {
100
+ const files = Array.isArray(options.files) ? options.files : [options.files]
101
+ for (const f of files) {
102
+ args.push('-f', f)
103
+ }
104
+ }
105
+
106
+ if (options.envFile) {
107
+ args.push('--env-file', options.envFile)
108
+ }
109
+
110
+ args.push('up')
111
+
112
+ if (options.detach !== false) {
113
+ args.push('-d')
114
+ }
115
+
116
+ if (options.build) {
117
+ args.push('--build')
118
+ }
119
+
120
+ if (options.flags) {
121
+ args.push(...options.flags)
122
+ }
123
+
124
+ if (options.services && options.services.length > 0) {
125
+ args.push(...options.services)
126
+ }
127
+
128
+ ctx?.logger.info(`Docker Compose up (services: ${options.services?.join(', ') ?? 'all'})`)
129
+ await shell.run({ cwd })`docker ${args}`
130
+ },
131
+
132
+ /**
133
+ * Stop and remove containers using Docker Compose.
134
+ */
135
+ async composeDown(options: ComposeOptions = {}): Promise<void> {
136
+ const ctx = tryGetContext()
137
+ const cwd = options.cwd ?? (ctx?.moduleFolder ? path.resolve(ctx.workspace, ctx.moduleFolder) : ctx?.workspace ?? process.cwd())
138
+ const args: string[] = ['compose']
139
+
140
+ if (options.files) {
141
+ const files = Array.isArray(options.files) ? options.files : [options.files]
142
+ for (const f of files) {
143
+ args.push('-f', f)
144
+ }
145
+ }
146
+
147
+ if (options.envFile) {
148
+ args.push('--env-file', options.envFile)
149
+ }
150
+
151
+ args.push('down')
152
+
153
+ if (options.flags) {
154
+ args.push(...options.flags)
155
+ }
156
+
157
+ if (options.services && options.services.length > 0) {
158
+ args.push(...options.services)
159
+ }
160
+
161
+ ctx?.logger.info('Docker Compose down')
162
+ await shell.run({ cwd })`docker ${args}`
163
+ }
164
+ }
@@ -0,0 +1,172 @@
1
+ import { execa } from 'execa'
2
+ import { tryGetContext } from '../context/index.js'
3
+
4
+ export interface GitCwdOptions {
5
+ dir?: string
6
+ }
7
+
8
+ export interface GitFetchOptions extends GitCwdOptions {
9
+ remote?: string
10
+ branch?: string
11
+ }
12
+
13
+ export interface GitResetOptions extends GitCwdOptions {
14
+ target?: string
15
+ }
16
+
17
+ export interface GitCheckoutOptions extends GitCwdOptions {
18
+ ref?: string
19
+ }
20
+
21
+ export interface GitCleanOptions extends GitCwdOptions {
22
+ forceIgnored?: boolean
23
+ }
24
+
25
+ function resolveGitCwd(optionsOrDir?: string | GitCwdOptions): string {
26
+ if (typeof optionsOrDir === 'string') {
27
+ return optionsOrDir
28
+ }
29
+ if (optionsOrDir?.dir) {
30
+ return optionsOrDir.dir
31
+ }
32
+ const ctx = tryGetContext()
33
+ if (ctx) {
34
+ return ctx.projectRoot ?? ctx.workspace ?? process.cwd()
35
+ }
36
+ return process.cwd()
37
+ }
38
+
39
+ export const git = {
40
+ /**
41
+ * Return full SHA of current commit.
42
+ * @param options Directory path or GitCwdOptions object. Defaults to repository root.
43
+ */
44
+ async currentCommit(options?: string | GitCwdOptions): Promise<string> {
45
+ const cwd = resolveGitCwd(options)
46
+ const { stdout } = await execa('git', ['rev-parse', 'HEAD'], { cwd })
47
+ return stdout.trim()
48
+ },
49
+
50
+ /**
51
+ * Return short SHA of current commit.
52
+ * @param options Directory path or GitCwdOptions object. Defaults to repository root.
53
+ */
54
+ async shortCommit(options?: string | GitCwdOptions): Promise<string> {
55
+ const cwd = resolveGitCwd(options)
56
+ const { stdout } = await execa('git', ['rev-parse', '--short', 'HEAD'], { cwd })
57
+ return stdout.trim()
58
+ },
59
+
60
+ /**
61
+ * Return current checked-out branch name, or empty string if detached HEAD.
62
+ * @param options Directory path or GitCwdOptions object. Defaults to repository root.
63
+ */
64
+ async currentBranch(options?: string | GitCwdOptions): Promise<string> {
65
+ const cwd = resolveGitCwd(options)
66
+ try {
67
+ const { stdout } = await execa('git', ['symbolic-ref', '--short', 'HEAD'], { cwd })
68
+ return stdout.trim()
69
+ } catch {
70
+ const isInside = await execa('git', ['rev-parse', '--is-inside-work-tree'], { cwd, reject: false })
71
+ if (!isInside.failed) {
72
+ return '' // Valid git repo, but detached HEAD
73
+ }
74
+ throw new Error(`Not a git repository: ${cwd}`)
75
+ }
76
+ },
77
+
78
+ /**
79
+ * Check if working tree has untracked or uncommitted changes.
80
+ * @param options Directory path or GitCwdOptions object. Defaults to repository root.
81
+ */
82
+ async isDirty(options?: string | GitCwdOptions): Promise<boolean> {
83
+ const cwd = resolveGitCwd(options)
84
+ const { stdout } = await execa('git', ['status', '--porcelain'], { cwd })
85
+ return stdout.trim().length > 0
86
+ },
87
+
88
+ /**
89
+ * Fetch latest changes from remote.
90
+ * @param options GitFetchOptions object { remote, branch, dir }. Defaults to { remote: 'origin' } at repo root.
91
+ */
92
+ async fetch(options: GitFetchOptions = {}): Promise<void> {
93
+ const cwd = resolveGitCwd(options)
94
+ const rem = options.remote ?? 'origin'
95
+ const br = options.branch
96
+
97
+ const ctx = tryGetContext()
98
+ if (ctx?.dryRun) {
99
+ ctx.logger.info(`[DRY-RUN] git fetch ${rem}${br ? ` ${br}` : ''}`)
100
+ return
101
+ }
102
+ const args = ['fetch', rem]
103
+ if (br) args.push(br)
104
+ await execa('git', args, { cwd })
105
+ },
106
+
107
+ /**
108
+ * Hard reset working tree to target commit/ref (defaults to HEAD).
109
+ * @param targetOrOptions Target ref name (string, e.g. 'HEAD', 'origin/main'), or GitResetOptions { target, dir }.
110
+ * Note: A bare string argument is treated as the target ref, NOT a directory. Pass { dir: '...' } to change directory.
111
+ */
112
+ async resetHard(targetOrOptions: string | GitResetOptions = 'HEAD'): Promise<void> {
113
+ let target = 'HEAD'
114
+ let cwd: string
115
+
116
+ if (typeof targetOrOptions === 'object' && targetOrOptions !== null) {
117
+ target = targetOrOptions.target ?? 'HEAD'
118
+ cwd = resolveGitCwd(targetOrOptions)
119
+ } else {
120
+ target = targetOrOptions
121
+ cwd = resolveGitCwd()
122
+ }
123
+
124
+ const ctx = tryGetContext()
125
+ if (ctx?.dryRun) {
126
+ ctx.logger.info(`[DRY-RUN] git reset --hard ${target}`)
127
+ return
128
+ }
129
+ await execa('git', ['reset', '--hard', target], { cwd })
130
+ },
131
+
132
+ /**
133
+ * Clean working tree untracked files.
134
+ * @param options GitCleanOptions { forceIgnored, dir }. Defaults to repo root.
135
+ */
136
+ async clean(options: GitCleanOptions = {}): Promise<void> {
137
+ const cwd = resolveGitCwd(options)
138
+ const ctx = tryGetContext()
139
+ const forceIgnored = options.forceIgnored ?? false
140
+ if (ctx?.dryRun) {
141
+ ctx.logger.info(`[DRY-RUN] git clean -fd${forceIgnored ? 'x' : ''}`)
142
+ return
143
+ }
144
+ const flags = forceIgnored ? ['clean', '-fdx'] : ['clean', '-fd']
145
+ await execa('git', flags, { cwd })
146
+ },
147
+
148
+ /**
149
+ * Switch or checkout a ref (defaults to main).
150
+ * @param refOrOptions Ref name (string, e.g. 'main', 'feature/login'), or GitCheckoutOptions { ref, dir }.
151
+ * Note: A bare string argument is treated as the ref name, NOT a directory. Pass { dir: '...' } to change directory.
152
+ */
153
+ async checkout(refOrOptions: string | GitCheckoutOptions = 'main'): Promise<void> {
154
+ let ref = 'main'
155
+ let cwd: string
156
+
157
+ if (typeof refOrOptions === 'object' && refOrOptions !== null) {
158
+ ref = refOrOptions.ref ?? 'main'
159
+ cwd = resolveGitCwd(refOrOptions)
160
+ } else {
161
+ ref = refOrOptions
162
+ cwd = resolveGitCwd()
163
+ }
164
+
165
+ const ctx = tryGetContext()
166
+ if (ctx?.dryRun) {
167
+ ctx.logger.info(`[DRY-RUN] git checkout ${ref}`)
168
+ return
169
+ }
170
+ await execa('git', ['checkout', ref], { cwd })
171
+ }
172
+ }
@@ -0,0 +1,4 @@
1
+ export * from './shell.js'
2
+ export * from './ssh.js'
3
+ export * from './git.js'
4
+ export * from './docker.js'