Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,23 @@ bunx open-workflows [OPTIONS]
OPTIONS
--skills Install skills only
--workflows Install workflows only
--force, -f Override existing files without prompts
--version, -v Display version
--help, -h Display help
```

### Override Behavior

By default, the CLI will prompt you to confirm overriding each existing file. Use `--force` to skip prompts and override all existing files automatically.

```bash
# Override all existing files
bunx open-workflows --force

# Interactive mode - prompts for each existing file
bunx open-workflows
```

## Plugin Installation

Add to your `opencode.json`:
Expand Down
23 changes: 23 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ This interactive installer will:
3. Install GitHub Actions to `.github/workflows/`
4. Create/update `.opencode/opencode.json`

### CLI Options

```bash
bunx open-workflows [OPTIONS]

OPTIONS
--skills Install skills only
--workflows Install workflows only
--force, -f Override existing files without prompts
--version, -v Display version
--help, -h Display help
```

By default, the CLI will prompt you to confirm overriding each existing file. Use `--force` to skip prompts and override all existing files automatically:

```bash
# Override all existing files
bunx open-workflows --force

# Interactive mode - prompts for each existing file
bunx open-workflows
```

## Manual Setup

### 1. Install the Plugin
Expand Down
91 changes: 84 additions & 7 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

import * as p from '@clack/prompts';
import color from 'picocolors';
import { installWorkflows, installSkills, installAuthWorkflow, createOpencodeConfig, type InstallResult } from './installer';
import {
installWorkflows,
installSkills,
installAuthWorkflow,
createOpencodeConfig,
checkExistingWorkflows,
checkExistingSkills,
checkExistingAuthWorkflow,
type InstallResult,
type ExistingFile,
} from './installer';
import type { WorkflowType } from './templates';

const pkg = await import('../../package.json').catch(() => ({ version: 'unknown' }));
Expand All @@ -13,6 +23,7 @@ const isHelp = args.includes('--help') || args.includes('-h');
const isVersion = args.includes('--version') || args.includes('-v');
const isSkillsOnly = args.includes('--skills');
const isWorkflowsOnly = args.includes('--workflows');
const isForce = args.includes('--force') || args.includes('-f');

if (isVersion) {
process.stdout.write(`@activade/open-workflows v${cliVersion}\n`);
Expand All @@ -30,6 +41,7 @@ USAGE
OPTIONS
--skills Install skills only (no workflows)
--workflows Install workflows only (no skills)
--force, -f Override existing files without prompts
--version, -v Display version
--help, -h Display this help

Expand All @@ -45,7 +57,7 @@ For more information: https://github.com/activadee/open-workflows

p.intro(color.bgCyan(color.black(` @activade/open-workflows v${cliVersion} `)));

const results = await p.group(
const promptResults = await p.group(
{
workflows: () =>
p.multiselect({
Expand Down Expand Up @@ -79,24 +91,81 @@ const results = await p.group(
}
);

const selectedWorkflows = (promptResults.workflows || []) as WorkflowType[];
const useOAuth = Boolean(promptResults.useOAuth);

const skillOverrides = new Set<string>();
const workflowOverrides = new Set<string>();
let overrideAuth = false;

if (!isForce) {
const existingFiles: ExistingFile[] = [];

if (!isWorkflowsOnly) {
existingFiles.push(...checkExistingSkills({}));
}

if (!isSkillsOnly) {
existingFiles.push(...checkExistingWorkflows({ workflows: selectedWorkflows }));
if (useOAuth) {
const authFile = checkExistingAuthWorkflow({});
if (authFile) {
existingFiles.push(authFile);
}
}
}

if (existingFiles.length > 0) {
p.log.warn(`Found ${existingFiles.length} existing file(s):`);

for (const file of existingFiles) {
const shouldOverride = await p.confirm({
message: `Override ${file.path}?`,
initialValue: false,
});

if (p.isCancel(shouldOverride)) {
p.cancel('Installation cancelled.');
process.exit(0);
}

if (shouldOverride) {
if (file.type === 'skill') {
skillOverrides.add(file.name);
} else if (file.type === 'workflow') {
workflowOverrides.add(file.name);
} else if (file.type === 'auth') {
overrideAuth = true;
}
}
}
}
}

const s = p.spinner();
s.start('Installing open-workflows...');

const allResults: InstallResult[] = [];
const selectedWorkflows = (results.workflows || []) as WorkflowType[];
const useOAuth = Boolean(results.useOAuth);

if (!isWorkflowsOnly) {
const skillResults = installSkills({});
const skillResults = installSkills({
override: isForce,
overrideNames: isForce ? undefined : skillOverrides,
});
allResults.push(...skillResults);
}

if (!isSkillsOnly) {
const workflowResults = installWorkflows({ workflows: selectedWorkflows, useOAuth });
const workflowResults = installWorkflows({
workflows: selectedWorkflows,
useOAuth,
override: isForce,
overrideNames: isForce ? undefined : workflowOverrides,
});
allResults.push(...workflowResults);

if (useOAuth) {
const authResult = installAuthWorkflow({});
const authResult = installAuthWorkflow({ override: isForce || overrideAuth });
allResults.push(authResult);
}
}
Expand All @@ -114,6 +183,7 @@ const hasErrors = allResults.some((r) => r.status === 'error');
s.stop(hasErrors ? 'Installation completed with errors' : 'Installation complete!');

const created = allResults.filter((r) => r.status === 'created');
const overwritten = allResults.filter((r) => r.status === 'overwritten');
const skipped = allResults.filter((r) => r.status === 'skipped');
const errors = allResults.filter((r) => r.status === 'error');

Expand All @@ -124,6 +194,13 @@ if (created.length > 0) {
}
}

if (overwritten.length > 0) {
p.log.success(`Overwritten ${overwritten.length} file(s):`);
for (const r of overwritten) {
p.log.message(` ${color.cyan('◆')} ${r.path}`);
}
}

if (skipped.length > 0) {
p.log.warn(`Skipped ${skipped.length} file(s) (already exist):`);
for (const r of skipped) {
Expand Down
97 changes: 82 additions & 15 deletions src/cli/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,68 @@ const WORKFLOW_GENERATORS: Record<string, (useOAuth: boolean) => string> = {
release: RELEASE,
};

export interface ExistingFile {
type: 'workflow' | 'skill' | 'auth';
name: string;
path: string;
}

export function checkExistingWorkflows(options: { workflows: WorkflowType[]; cwd?: string }): ExistingFile[] {
const { workflows, cwd = process.cwd() } = options;
const workflowDir = path.join(cwd, '.github', 'workflows');
const existing: ExistingFile[] = [];

for (const wf of workflows) {
const fileName = WORKFLOW_FILE_MAP[wf];
const filePath = path.join(workflowDir, `${fileName}.yml`);
if (fs.existsSync(filePath)) {
existing.push({
type: 'workflow',
name: wf,
path: `.github/workflows/${fileName}.yml`,
});
}
}

return existing;
}

export function checkExistingSkills(options: { cwd?: string }): ExistingFile[] {
const { cwd = process.cwd() } = options;
const targetDir = path.join(cwd, '.opencode', 'skill');
const existing: ExistingFile[] = [];

for (const name of SKILL_NAMES) {
const destPath = path.join(targetDir, name, 'SKILL.md');
if (fs.existsSync(destPath)) {
existing.push({
type: 'skill',
name,
path: `.opencode/skill/${name}/SKILL.md`,
});
}
}

return existing;
}

export function checkExistingAuthWorkflow(options: { cwd?: string }): ExistingFile | null {
const { cwd = process.cwd() } = options;
const filePath = path.join(cwd, '.github', 'workflows', 'opencode-auth.yml');
if (fs.existsSync(filePath)) {
return {
type: 'auth',
name: 'opencode-auth',
path: '.github/workflows/opencode-auth.yml',
};
}
return null;
}

export interface InstallResult {
type: 'workflow' | 'skill' | 'config' | 'auth';
name: string;
status: 'created' | 'skipped' | 'error';
status: 'created' | 'skipped' | 'overwritten' | 'error';
path: string;
message: string;
}
Expand All @@ -30,10 +88,12 @@ export interface InstallOptions {
workflows: WorkflowType[];
cwd?: string;
useOAuth?: boolean;
override?: boolean;
overrideNames?: Set<string>;
}

export function installWorkflows(options: InstallOptions): InstallResult[] {
const { workflows, cwd = process.cwd(), useOAuth = false } = options;
const { workflows, cwd = process.cwd(), useOAuth = false, override = false, overrideNames } = options;
const results: InstallResult[] = [];
const workflowDir = path.join(cwd, '.github', 'workflows');

Expand Down Expand Up @@ -70,7 +130,10 @@ export function installWorkflows(options: InstallOptions): InstallResult[] {
continue;
}

if (fs.existsSync(filePath)) {
const fileExists = fs.existsSync(filePath);
const shouldOverride = override || overrideNames?.has(wf);

if (fileExists && !shouldOverride) {
results.push({
type: 'workflow',
name: wf,
Expand All @@ -86,9 +149,9 @@ export function installWorkflows(options: InstallOptions): InstallResult[] {
results.push({
type: 'workflow',
name: wf,
status: 'created',
status: fileExists ? 'overwritten' : 'created',
path: `.github/workflows/${fileName}.yml`,
message: `Created successfully`,
message: fileExists ? 'Overwritten successfully' : 'Created successfully',
});
} catch (error) {
results.push({
Expand All @@ -104,16 +167,17 @@ export function installWorkflows(options: InstallOptions): InstallResult[] {
return results;
}

export function installAuthWorkflow(options: { cwd?: string }): InstallResult {
const { cwd = process.cwd() } = options;
export function installAuthWorkflow(options: { cwd?: string; override?: boolean }): InstallResult {
const { cwd = process.cwd(), override = false } = options;
const workflowDir = path.join(cwd, '.github', 'workflows');
const filePath = path.join(workflowDir, 'opencode-auth.yml');

if (!fs.existsSync(workflowDir)) {
fs.mkdirSync(workflowDir, { recursive: true });
}

if (fs.existsSync(filePath)) {
const fileExists = fs.existsSync(filePath);
if (fileExists && !override) {
return {
type: 'auth',
name: 'opencode-auth',
Expand All @@ -128,9 +192,9 @@ export function installAuthWorkflow(options: { cwd?: string }): InstallResult {
return {
type: 'auth',
name: 'opencode-auth',
status: 'created',
status: fileExists ? 'overwritten' : 'created',
path: '.github/workflows/opencode-auth.yml',
message: 'Created successfully',
message: fileExists ? 'Overwritten successfully' : 'Created successfully',
};
} catch (error) {
return {
Expand All @@ -143,16 +207,19 @@ export function installAuthWorkflow(options: { cwd?: string }): InstallResult {
}
}

export function installSkills(options: { cwd?: string }): InstallResult[] {
const { cwd = process.cwd() } = options;
export function installSkills(options: { cwd?: string; override?: boolean; overrideNames?: Set<string> }): InstallResult[] {
const { cwd = process.cwd(), override = false, overrideNames } = options;
const results: InstallResult[] = [];
const targetDir = path.join(cwd, '.opencode', 'skill');

for (const name of SKILL_NAMES) {
const skill = SKILLS[name];
const destPath = path.join(targetDir, name, 'SKILL.md');

if (fs.existsSync(destPath)) {
const fileExists = fs.existsSync(destPath);
const shouldOverride = override || overrideNames?.has(name);

if (fileExists && !shouldOverride) {
results.push({
type: 'skill',
name,
Expand All @@ -169,9 +236,9 @@ export function installSkills(options: { cwd?: string }): InstallResult[] {
results.push({
type: 'skill',
name,
status: 'created',
status: fileExists ? 'overwritten' : 'created',
path: `.opencode/skill/${name}/SKILL.md`,
message: `Created successfully`,
message: fileExists ? 'Overwritten successfully' : 'Created successfully',
});
} catch (error) {
results.push({
Expand Down
Loading