cc-orchestrator 0.2.7 → 0.2.9
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/README.md +17 -14
- package/index.js +98 -18
- package/lib/install-target.js +94 -0
- package/lib/release-target.js +137 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
# cc-orchestrator
|
|
2
2
|
|
|
3
|
-
One-line installer for [CC Orchestrator](https://github.com/zhsks311/cc-
|
|
3
|
+
One-line installer for [CC Orchestrator](https://github.com/zhsks311/cc-orchestrator) - Multi-model orchestration for Claude Code.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx cc-orchestrator
|
|
8
|
+
npx cc-orchestrator@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
That's it! The installer will:
|
|
12
|
-
1. Clone the repository to `~/.cc-
|
|
13
|
-
2.
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
12
|
+
1. Clone the repository to `~/.cc-orchestrator`
|
|
13
|
+
2. Check out the released git tag that matches the installer version
|
|
14
|
+
3. Install dependencies
|
|
15
|
+
4. Run the interactive setup wizard
|
|
16
|
+
5. Configure Claude Code automatically
|
|
16
17
|
|
|
17
18
|
## Usage
|
|
18
19
|
|
|
19
20
|
```bash
|
|
20
|
-
# Install to default location (~/.cc-
|
|
21
|
-
npx cc-orchestrator
|
|
21
|
+
# Install to default location (~/.cc-orchestrator)
|
|
22
|
+
npx cc-orchestrator@latest
|
|
22
23
|
|
|
23
24
|
# Install to custom directory
|
|
24
|
-
npx cc-orchestrator ./my-cco
|
|
25
|
+
npx cc-orchestrator@latest ./my-cco
|
|
25
26
|
|
|
26
|
-
# Update existing installation
|
|
27
|
-
npx cc-orchestrator --upgrade
|
|
27
|
+
# Update existing installation via the latest npm-published installer version
|
|
28
|
+
npx cc-orchestrator@latest --upgrade
|
|
28
29
|
|
|
29
30
|
# Force reinstall all components
|
|
30
|
-
npx cc-orchestrator --force
|
|
31
|
+
npx cc-orchestrator@latest --force
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
## After Installation
|
|
@@ -41,13 +42,15 @@ npx cc-orchestrator --force
|
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
44
|
# Option 1: Use npx
|
|
44
|
-
npx cc-orchestrator --upgrade
|
|
45
|
+
npx cc-orchestrator@latest --upgrade
|
|
45
46
|
|
|
46
47
|
# Option 2: Use npm script
|
|
47
|
-
cd ~/.cc-
|
|
48
|
+
cd ~/.cc-orchestrator
|
|
48
49
|
npm run update
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
`npm run update` checks the npm registry for the latest installer version, checks out the matching release tag, then runs `npm install` and `npm run setup -- --yes`.
|
|
53
|
+
|
|
51
54
|
## Requirements
|
|
52
55
|
|
|
53
56
|
- Node.js >= 18.0.0
|
package/index.js
CHANGED
|
@@ -15,9 +15,26 @@ import * as path from 'path';
|
|
|
15
15
|
import * as os from 'os';
|
|
16
16
|
import * as readline from 'readline';
|
|
17
17
|
import { execSync, spawn } from 'child_process';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import {
|
|
20
|
+
buildCloneCommand,
|
|
21
|
+
buildRemoteTagCheckCommand,
|
|
22
|
+
getMissingReleaseTagErrorMessage,
|
|
23
|
+
getReleaseTag,
|
|
24
|
+
runExistingInstallUpgradeWorkflow,
|
|
25
|
+
runFreshInstallWorkflow,
|
|
26
|
+
} from './lib/release-target.js';
|
|
27
|
+
import { classifyInstallTarget, resolveInstallTargetAction } from './lib/install-target.js';
|
|
18
28
|
|
|
19
29
|
const REPO_URL = 'https://github.com/zhsks311/cc-orchestrator.git';
|
|
20
30
|
const DEFAULT_INSTALL_DIR = path.join(os.homedir(), '.cc-orchestrator');
|
|
31
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
32
|
+
const __dirname = path.dirname(__filename);
|
|
33
|
+
const installerPackageJson = JSON.parse(
|
|
34
|
+
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')
|
|
35
|
+
);
|
|
36
|
+
const INSTALLER_VERSION = installerPackageJson.version;
|
|
37
|
+
const RELEASE_TAG = getReleaseTag(INSTALLER_VERSION);
|
|
21
38
|
|
|
22
39
|
// Parse arguments
|
|
23
40
|
const args = process.argv.slice(2);
|
|
@@ -27,7 +44,7 @@ const forceMode = args.includes('--force') || args.includes('-f');
|
|
|
27
44
|
const keysMode = args.includes('--keys') || args.includes('-k');
|
|
28
45
|
|
|
29
46
|
// Get custom directory from args (first non-flag arg)
|
|
30
|
-
const customDir = args.find(arg => !arg.startsWith('-'));
|
|
47
|
+
const customDir = args.find((arg) => !arg.startsWith('-'));
|
|
31
48
|
|
|
32
49
|
function printBanner() {
|
|
33
50
|
console.log(`
|
|
@@ -91,6 +108,41 @@ function exec(cmd, options = {}) {
|
|
|
91
108
|
execSync(cmd, { stdio: 'inherit', ...options });
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
function ensureRemoteReleaseTagExists(releaseTag) {
|
|
112
|
+
try {
|
|
113
|
+
execSync(buildRemoteTagCheckCommand(REPO_URL, releaseTag), {
|
|
114
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error?.status === 2) {
|
|
118
|
+
throw new Error(getMissingReleaseTagErrorMessage(releaseTag));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readPackageJsonName(installDir) {
|
|
126
|
+
try {
|
|
127
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(installDir, 'package.json'), 'utf8'));
|
|
128
|
+
return typeof packageJson?.name === 'string' ? packageJson.name : null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readRemoteOriginUrl(installDir) {
|
|
135
|
+
try {
|
|
136
|
+
return execSync('git config --get remote.origin.url', {
|
|
137
|
+
cwd: installDir,
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
140
|
+
}).trim();
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
94
146
|
function spawnAsync(cmd, args, options = {}) {
|
|
95
147
|
return new Promise((resolve, reject) => {
|
|
96
148
|
const proc = spawn(cmd, args, { stdio: 'inherit', shell: true, ...options });
|
|
@@ -105,33 +157,63 @@ function spawnAsync(cmd, args, options = {}) {
|
|
|
105
157
|
async function install(installDir) {
|
|
106
158
|
console.log(`\n📁 Install path: ${installDir}\n`);
|
|
107
159
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
160
|
+
const installDirExists = fs.existsSync(installDir);
|
|
161
|
+
const installTarget = classifyInstallTarget({
|
|
162
|
+
installDirExists,
|
|
163
|
+
gitDirExists: fs.existsSync(path.join(installDir, '.git')),
|
|
164
|
+
remoteOriginUrl: installDirExists ? readRemoteOriginUrl(installDir) : null,
|
|
165
|
+
packageJsonName: installDirExists ? readPackageJsonName(installDir) : null,
|
|
166
|
+
});
|
|
167
|
+
const installAction = resolveInstallTargetAction({ installTarget, upgradeMode });
|
|
168
|
+
|
|
169
|
+
if (installDirExists) {
|
|
170
|
+
if (installAction.action === 'upgrade_existing') {
|
|
111
171
|
console.log('📦 Existing installation found - upgrade mode\n');
|
|
112
|
-
} else {
|
|
172
|
+
} else if (installAction.confirmation === 'managed_overwrite') {
|
|
113
173
|
const answer = await question('⚠️ Already installed. Overwrite? (y/N): ');
|
|
114
174
|
if (answer.toLowerCase() !== 'y') {
|
|
115
175
|
console.log('\nInstallation cancelled.');
|
|
116
176
|
console.log('To upgrade: npx cc-orchestrator@latest --upgrade\n');
|
|
117
177
|
process.exit(0);
|
|
118
178
|
}
|
|
179
|
+
} else if (installAction.confirmation === 'explicit_delete') {
|
|
180
|
+
const answer = await question(
|
|
181
|
+
'⚠️ Existing directory is not managed by CC Orchestrator and will be deleted. Type DELETE to continue: '
|
|
182
|
+
);
|
|
183
|
+
if (answer !== 'DELETE') {
|
|
184
|
+
console.log('\nInstallation cancelled.');
|
|
185
|
+
console.log('To upgrade: npx cc-orchestrator@latest --upgrade\n');
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
119
188
|
}
|
|
120
189
|
}
|
|
121
190
|
|
|
191
|
+
if (installAction.action === 'abort_foreign_git') {
|
|
192
|
+
throw new Error(
|
|
193
|
+
'This directory is a git repository that is not managed by CC Orchestrator. Refusing to delete it.'
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
122
197
|
// Step 1: Clone or pull
|
|
123
198
|
console.log('─'.repeat(50));
|
|
124
|
-
if (
|
|
125
|
-
console.log(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
199
|
+
if (installAction.action === 'upgrade_existing') {
|
|
200
|
+
console.log(`\n[1/3] Checking out release ${RELEASE_TAG}...\n`);
|
|
201
|
+
runExistingInstallUpgradeWorkflow({
|
|
202
|
+
releaseTag: RELEASE_TAG,
|
|
203
|
+
runCommand: (command) => {
|
|
204
|
+
exec(command, { cwd: installDir });
|
|
205
|
+
},
|
|
206
|
+
});
|
|
129
207
|
} else {
|
|
130
|
-
console.log(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
208
|
+
console.log(`\n[1/3] Cloning release ${RELEASE_TAG}...\n`);
|
|
209
|
+
await runFreshInstallWorkflow({
|
|
210
|
+
installDirExists,
|
|
211
|
+
ensureRemoteReleaseTagExists: () => ensureRemoteReleaseTagExists(RELEASE_TAG),
|
|
212
|
+
removeExistingInstallDir: () => {
|
|
213
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
214
|
+
},
|
|
215
|
+
cloneRelease: () => exec(buildCloneCommand(REPO_URL, installDir, RELEASE_TAG)),
|
|
216
|
+
});
|
|
135
217
|
}
|
|
136
218
|
|
|
137
219
|
// Step 2: npm install
|
|
@@ -196,9 +278,7 @@ async function main() {
|
|
|
196
278
|
}
|
|
197
279
|
|
|
198
280
|
// Determine install directory
|
|
199
|
-
const installDir = customDir
|
|
200
|
-
? path.resolve(customDir)
|
|
201
|
-
: DEFAULT_INSTALL_DIR;
|
|
281
|
+
const installDir = customDir ? path.resolve(customDir) : DEFAULT_INSTALL_DIR;
|
|
202
282
|
|
|
203
283
|
await install(installDir);
|
|
204
284
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const MANAGED_REPO_SLUG = 'zhsks311/cc-orchestrator';
|
|
2
|
+
const MANAGED_PACKAGE_NAME = 'cc-orchestrator-server';
|
|
3
|
+
|
|
4
|
+
function stripGitSuffix(value) {
|
|
5
|
+
return value.replace(/\.git$/i, '').replace(/\/+$/u, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseGitHubSlug(remoteOriginUrl) {
|
|
9
|
+
if (typeof remoteOriginUrl !== 'string') {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const trimmed = remoteOriginUrl.trim();
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (trimmed.startsWith('git@github.com:')) {
|
|
19
|
+
return stripGitSuffix(trimmed.slice('git@github.com:'.length));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const parsed = new URL(trimmed);
|
|
24
|
+
if (parsed.hostname !== 'github.com') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return stripGitSuffix(parsed.pathname.replace(/^\/+/u, ''));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isManagedInstallRemoteUrl(remoteOriginUrl) {
|
|
35
|
+
return parseGitHubSlug(remoteOriginUrl) === MANAGED_REPO_SLUG;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function classifyInstallTarget({
|
|
39
|
+
installDirExists,
|
|
40
|
+
gitDirExists = false,
|
|
41
|
+
remoteOriginUrl,
|
|
42
|
+
packageJsonName,
|
|
43
|
+
}) {
|
|
44
|
+
if (!installDirExists) {
|
|
45
|
+
return 'missing';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!gitDirExists) {
|
|
49
|
+
return 'foreign_directory';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (isManagedInstallRemoteUrl(remoteOriginUrl) && packageJsonName === MANAGED_PACKAGE_NAME) {
|
|
53
|
+
return 'managed_install';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return 'foreign_git';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveInstallTargetAction({ installTarget, upgradeMode }) {
|
|
60
|
+
if (upgradeMode && installTarget !== 'managed_install') {
|
|
61
|
+
throw new Error('Upgrade mode is only supported for verified CC Orchestrator installations.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (installTarget === 'missing') {
|
|
65
|
+
return {
|
|
66
|
+
action: 'fresh_install',
|
|
67
|
+
confirmation: 'none',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (installTarget === 'managed_install') {
|
|
72
|
+
return upgradeMode
|
|
73
|
+
? {
|
|
74
|
+
action: 'upgrade_existing',
|
|
75
|
+
confirmation: 'none',
|
|
76
|
+
}
|
|
77
|
+
: {
|
|
78
|
+
action: 'fresh_install',
|
|
79
|
+
confirmation: 'managed_overwrite',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (installTarget === 'foreign_directory') {
|
|
84
|
+
return {
|
|
85
|
+
action: 'fresh_install',
|
|
86
|
+
confirmation: 'explicit_delete',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
action: 'abort_foreign_git',
|
|
92
|
+
confirmation: 'none',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export function ensureTagPrefix(value) {
|
|
2
|
+
return value.startsWith('v') ? value : `v${value}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getReleaseTag(version) {
|
|
6
|
+
return ensureTagPrefix(version.trim());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildCloneCommand(repoUrl, installDir, releaseTag) {
|
|
10
|
+
return `git clone --branch ${releaseTag} --depth 1 ${repoUrl} "${installDir}"`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildRemoteTagCheckCommand(repoUrl, releaseTag) {
|
|
14
|
+
return `git ls-remote --exit-code --tags ${repoUrl} refs/tags/${releaseTag}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getMissingReleaseTagErrorMessage(releaseTag) {
|
|
18
|
+
return `Release tag ${releaseTag} is not available yet. Retry after the release publish finishes.`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runFreshInstallWorkflow({
|
|
22
|
+
installDirExists,
|
|
23
|
+
ensureRemoteReleaseTagExists,
|
|
24
|
+
removeExistingInstallDir,
|
|
25
|
+
cloneRelease,
|
|
26
|
+
}) {
|
|
27
|
+
await ensureRemoteReleaseTagExists();
|
|
28
|
+
|
|
29
|
+
if (installDirExists) {
|
|
30
|
+
removeExistingInstallDir();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await cloneRelease();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildUpgradeCommandPlan(releaseTag) {
|
|
37
|
+
return {
|
|
38
|
+
fetchTags: 'git fetch --tags origin',
|
|
39
|
+
verifyReleaseTag: `git rev-parse -q --verify refs/tags/${releaseTag}`,
|
|
40
|
+
checkoutRelease: `git checkout --force ${releaseTag}`,
|
|
41
|
+
resetRelease: `git reset --hard ${releaseTag}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildUpgradeCommands(releaseTag) {
|
|
46
|
+
const plan = buildUpgradeCommandPlan(releaseTag);
|
|
47
|
+
return [plan.fetchTags, plan.verifyReleaseTag, plan.checkoutRelease, plan.resetRelease];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function runExistingInstallUpgradeWorkflow({ releaseTag, runCommand }) {
|
|
51
|
+
const plan = buildUpgradeCommandPlan(releaseTag);
|
|
52
|
+
|
|
53
|
+
runCommand(plan.fetchTags);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
runCommand(plan.verifyReleaseTag);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(getMissingReleaseTagErrorMessage(releaseTag));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
runCommand(plan.checkoutRelease);
|
|
62
|
+
runCommand(plan.resetRelease);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildSetupCommand() {
|
|
66
|
+
return 'npm run setup -- --yes';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildReleaseCommitRef(releaseTag) {
|
|
70
|
+
return `refs/tags/${releaseTag}^{commit}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildReleaseCommitLookupArgs(releaseTag) {
|
|
74
|
+
return ['rev-parse', buildReleaseCommitRef(releaseTag)];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getLatestVersionTag(tags) {
|
|
78
|
+
if (!Array.isArray(tags) || tags.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
[...tags]
|
|
84
|
+
.filter((tag) => /^v\d+\.\d+\.\d+$/.test(tag))
|
|
85
|
+
.sort((left, right) => right.localeCompare(left, undefined, { numeric: true }))[0] ?? null
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getLatestVersionTagFromOutput(output) {
|
|
90
|
+
if (typeof output !== 'string') {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return getLatestVersionTag(output.split('\n').map((line) => line.trim()).filter(Boolean));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getLatestVersionTagFromRemoteRefsOutput(output) {
|
|
98
|
+
if (typeof output !== 'string') {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const tags = output
|
|
103
|
+
.split('\n')
|
|
104
|
+
.map((line) => line.trim())
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.map((line) => line.split('\t')[1] ?? '')
|
|
107
|
+
.filter((ref) => ref.startsWith('refs/tags/'))
|
|
108
|
+
.map((ref) => ref.replace('refs/tags/', ''));
|
|
109
|
+
|
|
110
|
+
return getLatestVersionTag(tags);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getLatestPublishedInstallerReleaseTagFromOutput(output) {
|
|
114
|
+
if (typeof output !== 'string') {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const trimmed = output.trim();
|
|
119
|
+
if (!trimmed) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(trimmed);
|
|
125
|
+
if (typeof parsed === 'string' && parsed.trim()) {
|
|
126
|
+
return getReleaseTag(parsed);
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isReleaseCheckoutUpToDate(localCommit, releaseCommit) {
|
|
136
|
+
return Boolean(localCommit && releaseCommit && localCommit === releaseCommit);
|
|
137
|
+
}
|