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 CHANGED
@@ -1,33 +1,34 @@
1
1
  # cc-orchestrator
2
2
 
3
- One-line installer for [CC Orchestrator](https://github.com/zhsks311/cc-orchestratorestrator) - Multi-model orchestration for Claude Code.
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-orchestratorestrator`
13
- 2. Install dependencies
14
- 3. Run the interactive setup wizard
15
- 4. Configure Claude Code automatically
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-orchestratorestrator)
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-orchestratorestrator
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
- // Check if directory exists
109
- if (fs.existsSync(installDir)) {
110
- if (upgradeMode) {
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 (fs.existsSync(path.join(installDir, '.git'))) {
125
- console.log('\n[1/3] Fetching latest code...\n');
126
- // Use fetch + reset to handle local changes (e.g., package-lock.json from npm install)
127
- exec('git fetch origin main', { cwd: installDir });
128
- exec('git reset --hard origin/main', { cwd: installDir });
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('\n[1/3] Cloning repository...\n');
131
- if (fs.existsSync(installDir)) {
132
- fs.rmSync(installDir, { recursive: true, force: true });
133
- }
134
- exec(`git clone ${REPO_URL} "${installDir}"`);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-orchestrator",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "CLI to install CC Orchestrator - Multi-model orchestration for Claude Code",
5
5
  "bin": {
6
6
  "cc-orchestrator": "./index.js"