Skip to content

Commit 85ff5c3

Browse files
sestinjclaude
andcommitted
feat: initial implementation of create-software-factory CLI
Interactive CLI to scaffold and import .continue/agents/ and .continue/checks/ files from curated templates or any GitHub repo. - Interactive create flow with checkbox selection of checks and agents - GitHub import flow via --from owner/repo with frontmatter parsing - 4 curated check templates: code-review, security, test-coverage, pr-description - 3 curated agent templates: all-green, fix-issue, triage-issues - Semantic release workflow for npm publishing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0 parents  commit 85ff5c3

File tree

18 files changed

+830
-0
lines changed

18 files changed

+830
-0
lines changed

.github/workflows/release.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
release:
10+
name: Release
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
issues: write
15+
pull-requests: write
16+
id-token: write
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
persist-credentials: false
24+
25+
- name: Setup Node.js
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: "20"
29+
cache: "npm"
30+
31+
- name: Install dependencies
32+
run: npm ci
33+
34+
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
35+
run: npm audit signatures
36+
37+
- name: Release
38+
env:
39+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40+
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
41+
run: npx semantic-release

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
.env
3+
.env.*
4+
*.tgz

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# create-software-factory
2+
3+
Scaffold and import `.continue/agents/` and `.continue/checks/` files from curated templates or any GitHub repo.
4+
5+
## Quick Start
6+
7+
```bash
8+
npx create-software-factory
9+
```
10+
11+
This launches an interactive wizard that lets you pick from curated check and agent templates and writes them to your project's `.continue/` directory.
12+
13+
## Import from GitHub
14+
15+
Import checks and agents from any public GitHub repo:
16+
17+
```bash
18+
npx create-software-factory --from continuedev/remote-config-server
19+
```
20+
21+
For private repos, pass a GitHub token:
22+
23+
```bash
24+
npx create-software-factory --from myorg/private-repo --token ghp_xxxxx
25+
```
26+
27+
To fetch from a specific branch:
28+
29+
```bash
30+
npx create-software-factory --from owner/repo --branch develop
31+
```
32+
33+
## Options
34+
35+
```
36+
create-software-factory [options]
37+
38+
Options:
39+
--from <repo> Import from GitHub repo (owner/repo)
40+
--token <token> GitHub token for private repos
41+
--branch <branch> Branch to fetch from
42+
--dir <path> Target directory (default: cwd)
43+
-V, --version Output version number
44+
-h, --help Display help
45+
```
46+
47+
## Included Templates
48+
49+
### Checks
50+
51+
Checks run automatically on pull requests:
52+
53+
- **Code Review** — Reviews code quality, naming conventions, and patterns
54+
- **Security** — Scans for OWASP vulnerabilities, hardcoded secrets, and auth issues
55+
- **Test Coverage** — Ensures new code has appropriate test coverage
56+
- **PR Description** — Validates that PR descriptions cover all meaningful changes
57+
58+
### Agents
59+
60+
Agents are triggered by events or invoked on-demand:
61+
62+
- **All Green** — Fix checks, resolve conflicts, address reviews, and merge the PR (triggered by `ai-merge` label)
63+
- **Fix Issue** — Takes a GitHub issue and creates a fix PR (manual)
64+
- **Triage Issues** — Auto-label and respond to new issues (triggered on issue open)
65+
66+
## How It Works
67+
68+
### Interactive Create
69+
70+
Running without `--from` shows an interactive multi-select menu. You choose which checks and agents to install, and the selected template files are copied into your project's `.continue/checks/` and `.continue/agents/` directories.
71+
72+
### GitHub Import
73+
74+
Running with `--from owner/repo` fetches the `.continue/checks/` and `.continue/agents/` directories from the specified GitHub repository using the GitHub Contents API. It parses YAML frontmatter to show names and descriptions, lets you select which files to import, and writes them locally.
75+
76+
## License
77+
78+
Apache-2.0

bin/create-software-factory.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
import '../src/index.js';

package.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "create-software-factory",
3+
"version": "0.1.0",
4+
"description": "Scaffold and import .continue/agents and .continue/checks from curated templates or any GitHub repo",
5+
"type": "module",
6+
"bin": {
7+
"create-software-factory": "./bin/create-software-factory.js"
8+
},
9+
"files": [
10+
"bin",
11+
"src",
12+
"README.md",
13+
"CHANGELOG.md"
14+
],
15+
"scripts": {
16+
"build": "echo 'No build step needed - create-software-factory uses plain JavaScript'",
17+
"test": "echo \"Error: no test specified\" && exit 1"
18+
},
19+
"keywords": [
20+
"continue",
21+
"software-factory",
22+
"agents",
23+
"checks",
24+
"cli",
25+
"scaffold"
26+
],
27+
"author": "Continue",
28+
"license": "Apache-2.0",
29+
"repository": {
30+
"type": "git",
31+
"url": "https://github.com/continuedev/create-software-factory"
32+
},
33+
"dependencies": {
34+
"commander": "^12.0.0",
35+
"chalk": "^5.3.0",
36+
"ora": "^8.0.1",
37+
"node-fetch": "^3.3.2",
38+
"@inquirer/prompts": "^7.0.0",
39+
"yaml": "^2.6.0"
40+
},
41+
"devDependencies": {
42+
"@semantic-release/changelog": "^6.0.3",
43+
"@semantic-release/commit-analyzer": "^13.0.1",
44+
"@semantic-release/git": "^10.0.1",
45+
"@semantic-release/github": "^11.0.3",
46+
"@semantic-release/npm": "^12.0.2",
47+
"@semantic-release/release-notes-generator": "^14.0.3",
48+
"semantic-release": "^24.2.7"
49+
},
50+
"engines": {
51+
"node": ">=18.0.0"
52+
},
53+
"publishConfig": {
54+
"access": "public"
55+
}
56+
}

src/commands/create.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import { checkbox } from '@inquirer/prompts';
5+
import chalk from 'chalk';
6+
import { parseFrontmatter } from '../lib/frontmatter.js';
7+
import { writeFile } from '../lib/writer.js';
8+
9+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
10+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
11+
12+
const CHECKS = [
13+
{ file: 'code-review.md', name: 'Code Review', description: 'Reviews code quality and patterns' },
14+
{ file: 'security.md', name: 'Security', description: 'Scans for OWASP vulnerabilities, hardcoded secrets' },
15+
{ file: 'test-coverage.md', name: 'Test Coverage', description: 'Ensures new code has tests' },
16+
{ file: 'pr-description.md', name: 'PR Description', description: 'Validates PR descriptions' },
17+
];
18+
19+
const AGENTS = [
20+
{ file: 'all-green.md', name: 'All Green', description: 'Fix checks, resolve conflicts, address reviews, merge' },
21+
{ file: 'fix-issue.md', name: 'Fix Issue', description: 'Takes a GitHub issue and creates a fix PR' },
22+
{ file: 'triage-issues.md', name: 'Triage Issues', description: 'Auto-label and respond to new issues' },
23+
];
24+
25+
export function createCommand(program) {
26+
program
27+
.command('create', { isDefault: true })
28+
.description('Interactively scaffold .continue/checks and .continue/agents')
29+
.option('--dir <path>', 'Target directory', process.cwd())
30+
.action(async (opts) => {
31+
console.log();
32+
console.log(chalk.bold(' Continue Software Factory'));
33+
console.log();
34+
35+
const categories = await checkbox({
36+
message: 'What would you like to set up?',
37+
choices: [
38+
{ name: 'Checks (run automatically on PRs)', value: 'checks', checked: true },
39+
{ name: 'Agents (triggered by events or on-demand)', value: 'agents', checked: true },
40+
],
41+
});
42+
43+
if (categories.length === 0) {
44+
console.log(chalk.yellow('\n Nothing selected. Exiting.\n'));
45+
return;
46+
}
47+
48+
let selectedChecks = [];
49+
let selectedAgents = [];
50+
51+
if (categories.includes('checks')) {
52+
selectedChecks = await checkbox({
53+
message: 'Select checks:',
54+
choices: CHECKS.map((c) => ({
55+
name: `${c.name}${c.description}`,
56+
value: c.file,
57+
checked: false,
58+
})),
59+
});
60+
}
61+
62+
if (categories.includes('agents')) {
63+
selectedAgents = await checkbox({
64+
message: 'Select agents:',
65+
choices: AGENTS.map((a) => ({
66+
name: `${a.name}${a.description}`,
67+
value: a.file,
68+
checked: false,
69+
})),
70+
});
71+
}
72+
73+
const total = selectedChecks.length + selectedAgents.length;
74+
if (total === 0) {
75+
console.log(chalk.yellow('\n No files selected. Exiting.\n'));
76+
return;
77+
}
78+
79+
console.log();
80+
81+
let written = 0;
82+
83+
for (const file of selectedChecks) {
84+
const src = path.join(TEMPLATES_DIR, 'checks', file);
85+
const dest = path.join(opts.dir, '.continue', 'checks', file);
86+
const content = fs.readFileSync(src, 'utf-8');
87+
if (await writeFile(dest, content)) {
88+
written++;
89+
}
90+
}
91+
92+
for (const file of selectedAgents) {
93+
const src = path.join(TEMPLATES_DIR, 'agents', file);
94+
const dest = path.join(opts.dir, '.continue', 'agents', file);
95+
const content = fs.readFileSync(src, 'utf-8');
96+
if (await writeFile(dest, content)) {
97+
written++;
98+
}
99+
}
100+
101+
console.log(chalk.bold(`\n Done! ${written} file${written === 1 ? '' : 's'} created.\n`));
102+
});
103+
}

src/commands/import.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import path from 'node:path';
2+
import { checkbox } from '@inquirer/prompts';
3+
import chalk from 'chalk';
4+
import ora from 'ora';
5+
import { fetchDirectory, fetchFileContent } from '../lib/github.js';
6+
import { parseFrontmatter } from '../lib/frontmatter.js';
7+
import { writeFile } from '../lib/writer.js';
8+
9+
export function importCommand(program) {
10+
program
11+
.command('import')
12+
.description('Import .continue/checks and agents from a GitHub repo')
13+
.requiredOption('--from <repo>', 'GitHub repo (owner/repo)')
14+
.option('--token <token>', 'GitHub token for private repos')
15+
.option('--branch <branch>', 'Branch to fetch from')
16+
.option('--dir <path>', 'Target directory', process.cwd())
17+
.action(async (opts) => {
18+
const { from: repo, token, branch, dir } = opts;
19+
const fetchOpts = { token, branch };
20+
21+
console.log();
22+
const spinner = ora(`Fetching from ${repo}...`).start();
23+
24+
let checks, agents;
25+
try {
26+
[checks, agents] = await Promise.all([
27+
fetchDirectory(repo, '.continue/checks', fetchOpts),
28+
fetchDirectory(repo, '.continue/agents', fetchOpts),
29+
]);
30+
} catch (err) {
31+
spinner.fail(err.message);
32+
process.exit(1);
33+
}
34+
35+
spinner.succeed(`Found ${checks.length} check${checks.length === 1 ? '' : 's'}, ${agents.length} agent${agents.length === 1 ? '' : 's'}.`);
36+
37+
if (checks.length === 0 && agents.length === 0) {
38+
console.log(chalk.yellow('\n No .continue/checks or .continue/agents found in this repo.\n'));
39+
return;
40+
}
41+
42+
// Fetch content for all files to parse frontmatter for display names
43+
const spinnerContent = ora('Loading file metadata...').start();
44+
45+
const allFiles = [
46+
...checks.map((f) => ({ ...f, type: 'checks' })),
47+
...agents.map((f) => ({ ...f, type: 'agents' })),
48+
];
49+
50+
let fileContents;
51+
try {
52+
fileContents = await Promise.all(
53+
allFiles.map(async (f) => {
54+
const content = await fetchFileContent(repo, f.path, fetchOpts);
55+
const { data } = parseFrontmatter(content);
56+
return {
57+
name: f.name,
58+
path: f.path,
59+
category: f.type,
60+
displayName: data.name || f.name.replace(/\.md$/, ''),
61+
description: data.description || '',
62+
content,
63+
};
64+
}),
65+
);
66+
} catch (err) {
67+
spinnerContent.fail(err.message);
68+
process.exit(1);
69+
}
70+
71+
spinnerContent.succeed('Loaded file metadata.');
72+
console.log();
73+
74+
const choices = fileContents.map((f) => ({
75+
name: f.description
76+
? `${f.displayName}${f.description}`
77+
: f.displayName,
78+
value: f,
79+
checked: false,
80+
}));
81+
82+
const selected = await checkbox({
83+
message: 'Select files to import:',
84+
choices,
85+
});
86+
87+
if (selected.length === 0) {
88+
console.log(chalk.yellow('\n No files selected. Exiting.\n'));
89+
return;
90+
}
91+
92+
console.log();
93+
94+
let written = 0;
95+
for (const file of selected) {
96+
const dest = path.join(dir, '.continue', file.category, file.name);
97+
if (await writeFile(dest, file.content)) {
98+
written++;
99+
}
100+
}
101+
102+
console.log(chalk.bold(`\n Done! ${written} file${written === 1 ? '' : 's'} imported.\n`));
103+
});
104+
}

0 commit comments

Comments
 (0)