Skip to content

Commit 677274f

Browse files
committed
feat(install): implement --force flag to bypass version compatibility checks
Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent b848ba9 commit 677274f

File tree

5 files changed

+164
-8
lines changed

5 files changed

+164
-8
lines changed

postinstall.mjs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const flags = parseCliFlags(process.argv)
2929
const DRY_RUN = flags.dryRun
3030
const VERBOSE = flags.verbose
3131
const QUIET = flags.quiet
32+
const FORCE = flags.force
3233

3334
/** Print usage information and exit */
3435
if (flags.help) {
@@ -92,6 +93,12 @@ async function main() {
9293
verbose(`Source directory: ${AGENTS_SOURCE_DIR}`)
9394
verbose(`Target directory: ${AGENTS_TARGET_DIR}`)
9495
verbose(`Dry run: ${DRY_RUN}`)
96+
verbose(`Force: ${FORCE}`)
97+
98+
// Warn about force mode
99+
if (FORCE) {
100+
verbose(`Force mode enabled: version compatibility checks will be skipped`)
101+
}
95102

96103
// Create target directory if it doesn't exist
97104
if (!existsSync(AGENTS_TARGET_DIR)) {
@@ -150,10 +157,13 @@ async function main() {
150157
if (DRY_RUN) {
151158
// In dry-run mode, validate source file but don't copy
152159
verbose(` Validating source file (dry-run mode)...`)
153-
const validation = validateAgentFile(sourcePath)
160+
const validation = validateAgentFile(sourcePath, undefined, FORCE)
154161
if (!validation.valid) {
155162
throw new Error(`Invalid agent file content: ${validation.error}`)
156163
}
164+
if (validation.skippedVersionCheck) {
165+
verbose(` Warning: Version compatibility check skipped (--force)`)
166+
}
157167
verbose(` Validation passed`)
158168
successes.push(file)
159169
log(`${prefix}Would install: ${file} -> ${targetPath}`)
@@ -176,10 +186,13 @@ async function main() {
176186

177187
// Validate content structure
178188
verbose(` Validating content structure...`)
179-
const validation = validateAgentFile(targetPath)
189+
const validation = validateAgentFile(targetPath, undefined, FORCE)
180190
if (!validation.valid) {
181191
throw new Error(`Invalid agent file content: ${validation.error}`)
182192
}
193+
if (validation.skippedVersionCheck) {
194+
verbose(` Warning: Version compatibility check skipped (--force)`)
195+
}
183196
verbose(` Validation passed`)
184197

185198
successes.push(file)

preuninstall.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const flags = parseCliFlags(process.argv)
2828
const DRY_RUN = flags.dryRun
2929
const VERBOSE = flags.verbose
3030
const QUIET = flags.quiet
31+
const FORCE = flags.force
3132

3233
/** Print usage information and exit */
3334
if (flags.help) {
@@ -90,6 +91,12 @@ async function main() {
9091
verbose(`Source directory: ${AGENTS_SOURCE_DIR}`)
9192
verbose(`Target directory: ${AGENTS_TARGET_DIR}`)
9293
verbose(`Dry run: ${DRY_RUN}`)
94+
verbose(`Force: ${FORCE}`)
95+
96+
// Note about force mode for uninstall
97+
if (FORCE) {
98+
verbose(`Force mode enabled: removal will proceed without additional checks`)
99+
}
93100

94101
// Check if target directory exists
95102
verbose(`Checking if target directory exists...`)

src/paths.d.mts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,8 @@ export interface ValidateAgentFileResult {
397397
valid: boolean
398398
/** Error message if validation failed */
399399
error?: string
400+
/** Whether version compatibility check was skipped (when force=true and version is incompatible) */
401+
skippedVersionCheck?: boolean
400402
}
401403

402404
/**
@@ -405,10 +407,11 @@ export interface ValidateAgentFileResult {
405407
*
406408
* Performs the following validations:
407409
* 1. Content structure validation (frontmatter, headers, keywords)
408-
* 2. Version compatibility checking against current OpenCode version
410+
* 2. Version compatibility checking against current OpenCode version (unless force=true)
409411
*
410412
* @param filePath - Path to the agent file to validate
411413
* @param currentVersion - The current OpenCode version to check against (defaults to OPENCODE_VERSION)
414+
* @param force - When true, skip version compatibility checks (default: false)
412415
* @returns Validation result with valid status and optional error message
413416
* @throws {Error} If the file does not exist (ENOENT)
414417
* @throws {Error} If permission is denied reading the file (EACCES)
@@ -434,5 +437,12 @@ export interface ValidateAgentFileResult {
434437
* @example
435438
* // Validate against a specific version
436439
* const result = validateAgentFile('/path/to/agent.md', '1.0.0')
440+
*
441+
* @example
442+
* // Force install, skipping version compatibility check
443+
* const result = validateAgentFile('/path/to/agent.md', '1.0.0', true)
444+
* if (result.skippedVersionCheck) {
445+
* console.warn('Warning: Version compatibility check was skipped')
446+
* }
437447
*/
438-
export function validateAgentFile(filePath: string, currentVersion?: string): ValidateAgentFileResult
448+
export function validateAgentFile(filePath: string, currentVersion?: string, force?: boolean): ValidateAgentFileResult

src/paths.mjs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -557,11 +557,12 @@ export function createLogger(verbose, quiet = false) {
557557
*
558558
* Performs the following validations:
559559
* 1. Content structure validation (frontmatter, headers, keywords)
560-
* 2. Version compatibility checking against current OpenCode version
560+
* 2. Version compatibility checking against current OpenCode version (unless force=true)
561561
*
562562
* @param {string} filePath - Path to the agent file to validate
563563
* @param {string} [currentVersion] - The current OpenCode version to check against (defaults to OPENCODE_VERSION)
564-
* @returns {{ valid: boolean, error?: string }} Validation result with optional error message
564+
* @param {boolean} [force=false] - When true, skip version compatibility checks
565+
* @returns {{ valid: boolean, error?: string, skippedVersionCheck?: boolean }} Validation result with optional error message
565566
* @throws {Error} If the file does not exist (ENOENT)
566567
* @throws {Error} If permission is denied reading the file (EACCES)
567568
* @throws {Error} If the file is a directory (EISDIR)
@@ -586,8 +587,15 @@ export function createLogger(verbose, quiet = false) {
586587
* @example
587588
* // Validate against a specific version
588589
* const result = validateAgentFile('/path/to/agent.md', '1.0.0')
590+
*
591+
* @example
592+
* // Force install, skipping version compatibility check
593+
* const result = validateAgentFile('/path/to/agent.md', '1.0.0', true)
594+
* if (result.skippedVersionCheck) {
595+
* console.warn('Warning: Version compatibility check was skipped')
596+
* }
589597
*/
590-
export function validateAgentFile(filePath, currentVersion = OPENCODE_VERSION) {
598+
export function validateAgentFile(filePath, currentVersion = OPENCODE_VERSION, force = false) {
591599
if (typeof filePath !== "string") {
592600
throw new TypeError(
593601
`validateAgentFile: filePath must be a string, got ${filePath === null ? "null" : typeof filePath}`,
@@ -603,12 +611,16 @@ export function validateAgentFile(filePath, currentVersion = OPENCODE_VERSION) {
603611
return contentValidation
604612
}
605613

606-
// Check version compatibility from frontmatter
614+
// Check version compatibility from frontmatter (unless force is true)
607615
const frontmatter = parseFrontmatter(content)
608616
if (frontmatter.found && frontmatter.fields.requires) {
609617
const requiresVersion = frontmatter.fields.requires
610618
const isCompatible = _checkVersionCompatibility(requiresVersion, currentVersion)
611619
if (!isCompatible) {
620+
if (force) {
621+
// Skip version check when force is enabled, but indicate it was skipped
622+
return { valid: true, skippedVersionCheck: true }
623+
}
612624
return {
613625
valid: false,
614626
error: `Incompatible OpenCode version: requires ${requiresVersion}, but current version is ${currentVersion}`,

tests/paths.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,5 +3019,119 @@ This is a test agent that handles various tasks.
30193019
}
30203020
})
30213021
})
3022+
3023+
describe("force parameter", () => {
3024+
it("should skip version compatibility check when force is true", () => {
3025+
const { writeFileSync, unlinkSync, existsSync: fsExistsSync } = require("node:fs")
3026+
const tempPath = join(import.meta.dirname, "temp-force-version.md")
3027+
try {
3028+
const content = `---
3029+
version: 1.0
3030+
requires: ">=99.0.0"
3031+
---
3032+
# Test Agent
3033+
3034+
This is a test agent that handles various tasks.
3035+
`.padEnd(MIN_CONTENT_LENGTH + 50, " ")
3036+
writeFileSync(tempPath, content)
3037+
3038+
// Without force, this should fail
3039+
const resultWithoutForce = validateAgentFile(tempPath, "1.0.0", false)
3040+
expect(resultWithoutForce.valid).toBe(false)
3041+
expect(resultWithoutForce.error).toContain("Incompatible OpenCode version")
3042+
3043+
// With force, this should succeed and indicate version check was skipped
3044+
const resultWithForce = validateAgentFile(tempPath, "1.0.0", true)
3045+
expect(resultWithForce.valid).toBe(true)
3046+
expect(resultWithForce.skippedVersionCheck).toBe(true)
3047+
} finally {
3048+
if (fsExistsSync(tempPath)) {
3049+
unlinkSync(tempPath)
3050+
}
3051+
}
3052+
})
3053+
3054+
it("should not set skippedVersionCheck when version is compatible", () => {
3055+
const { writeFileSync, unlinkSync, existsSync: fsExistsSync } = require("node:fs")
3056+
const tempPath = join(import.meta.dirname, "temp-force-compatible.md")
3057+
try {
3058+
const content = `---
3059+
version: 1.0
3060+
requires: ">=1.0.0"
3061+
---
3062+
# Test Agent
3063+
3064+
This is a test agent that handles various tasks.
3065+
`.padEnd(MIN_CONTENT_LENGTH + 50, " ")
3066+
writeFileSync(tempPath, content)
3067+
3068+
// Version is compatible, so skippedVersionCheck should not be set
3069+
const result = validateAgentFile(tempPath, "1.0.0", true)
3070+
expect(result.valid).toBe(true)
3071+
expect(result.skippedVersionCheck).toBeUndefined()
3072+
} finally {
3073+
if (fsExistsSync(tempPath)) {
3074+
unlinkSync(tempPath)
3075+
}
3076+
}
3077+
})
3078+
3079+
it("should still validate content structure when force is true", () => {
3080+
const { writeFileSync, unlinkSync, existsSync: fsExistsSync } = require("node:fs")
3081+
const tempPath = join(import.meta.dirname, "temp-force-invalid-content.md")
3082+
try {
3083+
// Content without frontmatter should still fail even with force
3084+
writeFileSync(
3085+
tempPath,
3086+
"# No Frontmatter\n\nThis agent has no frontmatter but mentions agent and task.".padEnd(
3087+
MIN_CONTENT_LENGTH + 10,
3088+
" ",
3089+
),
3090+
)
3091+
const result = validateAgentFile(tempPath, "1.0.0", true)
3092+
expect(result.valid).toBe(false)
3093+
expect(result.error).toContain("frontmatter")
3094+
} finally {
3095+
if (fsExistsSync(tempPath)) {
3096+
unlinkSync(tempPath)
3097+
}
3098+
}
3099+
})
3100+
3101+
it("should default force to false when not provided", () => {
3102+
const { writeFileSync, unlinkSync, existsSync: fsExistsSync } = require("node:fs")
3103+
const tempPath = join(import.meta.dirname, "temp-force-default.md")
3104+
try {
3105+
const content = `---
3106+
version: 1.0
3107+
requires: ">=99.0.0"
3108+
---
3109+
# Test Agent
3110+
3111+
This is a test agent that handles various tasks.
3112+
`.padEnd(MIN_CONTENT_LENGTH + 50, " ")
3113+
writeFileSync(tempPath, content)
3114+
3115+
// Without explicit force parameter, should fail on incompatible version
3116+
const result = validateAgentFile(tempPath, "1.0.0")
3117+
expect(result.valid).toBe(false)
3118+
expect(result.error).toContain("Incompatible OpenCode version")
3119+
} finally {
3120+
if (fsExistsSync(tempPath)) {
3121+
unlinkSync(tempPath)
3122+
}
3123+
}
3124+
})
3125+
3126+
it("should work with actual agent files when force is true", () => {
3127+
const agentsDir = join(import.meta.dirname, "..", "agents")
3128+
for (const name of AGENT_NAMES) {
3129+
const filePath = join(agentsDir, `${name}.md`)
3130+
const result = validateAgentFile(filePath, "0.0.1", true)
3131+
// With force=true, even incompatible versions should pass
3132+
expect(result.valid).toBe(true)
3133+
}
3134+
})
3135+
})
30223136
})
30233137
})

0 commit comments

Comments
 (0)