@ticatec/omniflow-core 0.1.1 → 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.
- package/LICENSE +21 -0
- package/README.md +112 -54
- package/README_CN.md +113 -55
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/plugin/PluginContext.d.ts +39 -0
- package/dist/plugin/PluginContext.d.ts.map +1 -0
- package/dist/plugin/PluginContext.js +8 -0
- package/dist/plugin/PluginContext.js.map +1 -0
- package/dist/primitives/docker.d.ts.map +1 -1
- package/dist/primitives/docker.js +9 -0
- package/dist/primitives/docker.js.map +1 -1
- package/dist/primitives/git.d.ts +58 -16
- package/dist/primitives/git.d.ts.map +1 -1
- package/dist/primitives/git.js +91 -33
- package/dist/primitives/git.js.map +1 -1
- package/dist/primitives/shell.d.ts +4 -0
- package/dist/primitives/shell.d.ts.map +1 -1
- package/dist/primitives/shell.js +20 -60
- package/dist/primitives/shell.js.map +1 -1
- package/dist/primitives/ssh.d.ts +8 -5
- package/dist/primitives/ssh.d.ts.map +1 -1
- package/dist/primitives/ssh.js +49 -14
- package/dist/primitives/ssh.js.map +1 -1
- package/dist/primitives/subprocess.d.ts +52 -0
- package/dist/primitives/subprocess.d.ts.map +1 -0
- package/dist/primitives/subprocess.js +353 -0
- package/dist/primitives/subprocess.js.map +1 -0
- package/dist/toolchain/providers/GradleToolchain.d.ts +1 -1
- package/dist/toolchain/providers/GradleToolchain.d.ts.map +1 -1
- package/dist/toolchain/providers/GradleToolchain.js +4 -3
- package/dist/toolchain/providers/GradleToolchain.js.map +1 -1
- package/dist/toolchain/providers/MavenToolchain.d.ts +1 -1
- package/dist/toolchain/providers/MavenToolchain.d.ts.map +1 -1
- package/dist/toolchain/providers/MavenToolchain.js +3 -3
- package/dist/toolchain/providers/MavenToolchain.js.map +1 -1
- package/dist/toolchain/providers/NodeToolchain.d.ts +1 -1
- package/dist/toolchain/providers/NodeToolchain.d.ts.map +1 -1
- package/dist/toolchain/providers/NodeToolchain.js +61 -0
- package/dist/toolchain/providers/NodeToolchain.js.map +1 -1
- package/dist/toolchain/providers/pom.d.ts.map +1 -1
- package/dist/toolchain/providers/pom.js +13 -0
- package/dist/toolchain/providers/pom.js.map +1 -1
- package/dist/toolchain/registry.d.ts +11 -3
- package/dist/toolchain/registry.d.ts.map +1 -1
- package/dist/toolchain/registry.js +33 -8
- package/dist/toolchain/registry.js.map +1 -1
- package/dist/utils/mask.d.ts.map +1 -1
- package/dist/utils/mask.js +30 -5
- package/dist/utils/mask.js.map +1 -1
- package/docs/toolchain-extension.md +301 -0
- package/docs/toolchain-extension_CN.md +304 -0
- package/package.json +23 -1
- package/src/context/index.ts +74 -0
- package/src/context/storage.ts +8 -0
- package/src/context/types.ts +69 -0
- package/src/index.ts +97 -0
- package/src/plugin/PluginContext.ts +57 -0
- package/src/primitives/docker.ts +164 -0
- package/src/primitives/git.ts +172 -0
- package/src/primitives/index.ts +4 -0
- package/src/primitives/shell.ts +157 -0
- package/src/primitives/ssh.ts +249 -0
- package/src/primitives/subprocess.ts +389 -0
- package/src/toolchain/index.ts +6 -0
- package/src/toolchain/providers/GradleToolchain.ts +137 -0
- package/src/toolchain/providers/MavenToolchain.ts +64 -0
- package/src/toolchain/providers/NodeToolchain.ts +172 -0
- package/src/toolchain/providers/pom.ts +145 -0
- package/src/toolchain/registry.ts +161 -0
- package/src/toolchain/types.ts +40 -0
- package/src/utils/mask.ts +73 -0
- 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
|
+
}
|