Skip to content
Open
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
100 changes: 100 additions & 0 deletions AI_AUTOMATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# AI Automation Support

This document describes the AI automation features added to quick-publish.

## New CLI Options

### `--dry-run`
Shows what commands would be executed without actually running them. Perfect for AI agents to understand what will happen before execution.

```bash
# See what would happen with a patch release
quick-publish --dry-run --release-type patch

# Preview a custom version release
quick-publish --dry-run --release-type custom --version 1.2.3 --tag beta
```

### `--yes`
Skips all interactive prompts and uses provided or default values. Enables fully automated execution.

```bash
# Automated patch release
quick-publish --yes --release-type patch

# Automated custom release
quick-publish --yes --release-type custom --version 1.2.3 --tag latest
```

### Version and Tag Options

- `--release-type <type>`: `patch`, `minor`, `major`, `prerelease`, or `custom`
- `--version <version>`: Custom version (required when `--release-type=custom`)
- `--tag <tag>`: NPM tag (`latest`, `next`, `beta`, or custom tag name)

## AI Usage Examples

### 1. Preview Changes (Dry Run)
```bash
# AI can run this to see what would happen
quick-publish --dry-run --release-type patch
```

Output:
```
[DRY RUN] Would select version: 0.7.2, tag: latest
[DRY RUN] Would ask: Continue to publish `0.7.2` with tag `latest`? (auto-yes in automated mode)

[DRY RUN] Publish workflow for version 0.7.2 with tag latest:
[DRY RUN] Would execute: npm version 0.7.2
[DRY RUN] Would execute: /path/to/conventional-changelog -p angular -r 2 -i CHANGELOG.md -s
[DRY RUN] Would execute: npm publish --tag=latest
[DRY RUN] Would execute: git add CHANGELOG.md
[DRY RUN] Would execute: git commit -m chore%3A%20changelog%200.7.2
[DRY RUN] Would execute: git push
[DRY RUN] Would execute: git push origin refs/tags/v0.7.2

[DRY RUN] Publish workflow completed. Use --yes to execute.
```

### 2. Automated Execution
```bash
# AI can run this after confirming with dry-run
quick-publish --yes --release-type patch
```

### 3. Custom Version Release
```bash
# For specific version requirements
quick-publish --yes --release-type custom --version 1.0.0 --tag latest
```

### 4. Beta Release
```bash
# For pre-release versions
quick-publish --yes --release-type prerelease --tag beta
```

## AI Workflow Recommendation

1. **Analysis Phase**: AI runs with `--dry-run` to understand what will happen
2. **Confirmation Phase**: AI presents the planned actions to user
3. **Execution Phase**: AI runs with `--yes` and appropriate parameters

```bash
# Step 1: Analyze
quick-publish --dry-run --release-type patch

# Step 2: Execute (after user confirmation)
quick-publish --yes --release-type patch
```

## Error Handling

- Missing `--version` with `--release-type=custom` will throw an error
- Invalid release types will be rejected
- All validation happens before any commands are executed

## Backward Compatibility

All existing functionality remains unchanged. The new options are additive and don't affect the interactive mode when not specified.
20 changes: 20 additions & 0 deletions src/cli.ts
100755 β†’ 100644
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ export function bootstrapCli() {
'--push',
'Execute git push & tag push to remote git origin, defaults to `true`',
)
.option(
'--dry-run',
'Show what would be executed without actually running commands',
)
.option(
'--yes',
'Skip all prompts and use default values for automated execution',
)
.option(
'--release-type <type>',
'Release type: patch, minor, major, prerelease, or custom',
)
.option(
'--version <version>',
'Custom version (required when --release-type=custom)',
)
.option(
'--tag <tag>',
'NPM tag: latest, next, beta, or custom tag name',
)
.action(opts => {
publish(opts);
});
Expand Down
63 changes: 45 additions & 18 deletions src/index.ts
100755 β†’ 100644
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export async function publish(opts: IOptions = {}) {
opts = {
depcost: false,
push: true,
dryRun: false,
yes: false,
...opts,
};

Expand All @@ -24,36 +26,61 @@ export async function publish(opts: IOptions = {}) {
*/
const { version, tag } = await selectVersionAndTag(
require(`${process.cwd()}/package.json`).version,
{
yes: opts.yes,
dryRun: opts.dryRun,
releaseType: opts.releaseType,
version: opts.version,
tag: opts.tag,
},
);

/**
* 2. Double check.
* 2. Double check (skip in automated mode).
*/
const { continueTo } = await inquirer.prompt<{
continueTo: boolean;
}>({
type: 'confirm',
name: 'continueTo',
message: `Continue to publish \`${version}\` with tag \`${tag}\`?`,
});
let continueTo = true;
if (!opts.yes && !opts.dryRun) {
const result = await inquirer.prompt<{
continueTo: boolean;
}>({
type: 'confirm',
name: 'continueTo',
message: `Continue to publish \`${version}\` with tag \`${tag}\`?`,
});
continueTo = result.continueTo;
} else if (opts.dryRun) {
console.log(`[DRY RUN] Would ask: Continue to publish \`${version}\` with tag \`${tag}\`? (auto-yes in automated mode)`);
}

/**
* 3. Publish workflow.
*/
if (continueTo) {
await exec(COMMANDS.bumpVersion(version));
await exec(COMMANDS.changelog());
await exec(COMMANDS.npmPublish(tag));
await exec(COMMANDS.gitAdd('CHANGELOG.md'));
await exec(COMMANDS.gitCommit(`chore: changelog ${version}`));
if (opts.dryRun) {
console.log(`\n[DRY RUN] Publish workflow for version ${version} with tag ${tag}:`);
}

await exec(COMMANDS.bumpVersion(version), opts.dryRun);
await exec(COMMANDS.changelog(), opts.dryRun);
await exec(COMMANDS.npmPublish(tag), opts.dryRun);
await exec(COMMANDS.gitAdd('CHANGELOG.md'), opts.dryRun);
await exec(COMMANDS.gitCommit(`chore: changelog ${version}`), opts.dryRun);

if (opts.depcost) {
await exec(COMMANDS.depcost());
await exec(COMMANDS.gitAdd('DEPCOST.md'));
await exec(COMMANDS.gitCommit(`chore: DEPCOST.md ${version}`));
await exec(COMMANDS.depcost(), opts.dryRun);
await exec(COMMANDS.gitAdd('DEPCOST.md'), opts.dryRun);
await exec(COMMANDS.gitCommit(`chore: DEPCOST.md ${version}`), opts.dryRun);
}

if (opts.push) {
await exec(COMMANDS.gitPush());
await exec(COMMANDS.gitPushTag(`v${version}`));
await exec(COMMANDS.gitPush(), opts.dryRun);
await exec(COMMANDS.gitPushTag(`v${version}`), opts.dryRun);
}

if (opts.dryRun) {
console.log(`\n[DRY RUN] Publish workflow completed. Use --yes to execute.`);
} else {
console.log(`\nPublish completed: ${version} with tag ${tag}`);
}
} else {
console.log('Publish cancelled');
Expand Down
35 changes: 35 additions & 0 deletions src/select-version.ts
100755 β†’ 100644
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import type { ISelectVersionAndTagResult, VersionCandidate, IPromptAnswers, Rele
*/
export async function selectVersionAndTag(
currentVersion: string,
opts: {
yes?: boolean;
dryRun?: boolean;
releaseType?: string;
version?: string;
tag?: string;
} = {},
): Promise<ISelectVersionAndTagResult> {
const customItem = { name: 'Custom', value: 'custom' };

Expand Down Expand Up @@ -56,6 +63,34 @@ export async function selectVersionAndTag(
return Boolean(semver.prerelease(version));
}

// Handle automated execution
if (opts.yes || opts.dryRun) {
const releaseType = (opts.releaseType as ReleaseType) || (isPreRelease(currentVersion) ? 'prerelease' : 'patch');
let version: string;

if (releaseType === 'custom') {
if (!opts.version) {
throw new Error('--version is required when --release-type=custom');
}
version = opts.version;
} else {
version = versionCandidate[releaseType]!;
}

const npmTags = getNpmTags(version);
let tag = opts.tag;
if (!tag) {
const firstTag = npmTags[0];
tag = typeof firstTag === 'string' ? firstTag : firstTag.value;
}

if (opts.dryRun) {
console.log(`[DRY RUN] Would select version: ${version}, tag: ${tag}`);
}

return { version, tag };
}

const bumpChoices = releaseTypes.map(b => ({
name: `${b} (${versionCandidate[b]})`,
value: b,
Expand Down
8 changes: 7 additions & 1 deletion src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ import execa from 'execa';
* Execute a inline command.
*
* @param command
* @param dryRun - If true, only log the command without executing
* @returns
*/
export function exec(command: string) {
export function exec(command: string, dryRun: boolean = false) {
if (dryRun) {
console.log(`[DRY RUN] Would execute: ${command}`);
return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 });
}

const splits = command.split(' ');
return execa(
splits[0],
Expand Down
20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ export interface IOptions {
* Execute git push & tag push to remote git origin, defaults to `true`
*/
push?: boolean;
/**
* Show what would be executed without actually running commands
*/
dryRun?: boolean;
/**
* Skip all prompts and use default values for automated execution
*/
yes?: boolean;
/**
* Release type: patch, minor, major, prerelease, or custom
*/
releaseType?: ReleaseType | CustomReleaseType;
/**
* Custom version (required when releaseType=custom)
*/
version?: string;
/**
* NPM tag: latest, next, beta, or custom tag name
*/
tag?: string;
}
/**
* Definition of versions candidate.
Expand Down
8 changes: 5 additions & 3 deletions tsconfig.json
100755 β†’ 100644
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
"downlevelIteration": true,
"lib": [
"es5",
"es6"
]
"es6",
"dom"
],
"types": ["node"]
},
"include": [
"src",
Expand All @@ -39,4 +41,4 @@
"exclude": [
"node_modules"
]
}
}
Loading