@reyemtech/stack-upgrade 0.4.0 → 0.5.0
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 +67 -0
- package/package.json +2 -2
- package/src/github.js +37 -30
- package/src/index.js +28 -19
- package/src/prompts.js +0 -13
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @reyemtech/stack-upgrade
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@reyemtech/stack-upgrade)
|
|
4
|
+
[](https://www.npmjs.com/package/@reyemtech/stack-upgrade)
|
|
5
|
+
[](https://github.com/ReyemTech/stack-upgrade/blob/main/LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
|
|
8
|
+
CLI to launch Stack Upgrade Agents via Docker or Kubernetes. Auto-detects repos, Claude credentials, and runtime — then launches disposable containers that upgrade your stack autonomously using [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
|
9
|
+
|
|
10
|
+
**Currently supports:** Laravel. **Coming soon:** React, Rails, Django.
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @reyemtech/stack-upgrade
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or install globally:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm i -g @reyemtech/stack-upgrade
|
|
22
|
+
stack-upgrade
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- **Docker** or **kubectl** in PATH
|
|
28
|
+
- **GitHub CLI** (`gh`) authenticated — for repo discovery and PR creation
|
|
29
|
+
- **Claude credentials** — Claude Max token or Anthropic API key
|
|
30
|
+
|
|
31
|
+
## What It Does
|
|
32
|
+
|
|
33
|
+
1. Scans your GitHub repos for supported stacks (Laravel via `composer.json`)
|
|
34
|
+
2. Prompts for target version, push/PR preference, and branch suffix
|
|
35
|
+
3. Supports queuing multiple upgrades to run in parallel
|
|
36
|
+
4. Launches a disposable Docker container (or K8s pod) per upgrade
|
|
37
|
+
5. Each container runs Claude Code autonomously through upgrade phases
|
|
38
|
+
6. Pushes an upgrade branch and optionally opens a PR with a generated changelog
|
|
39
|
+
|
|
40
|
+
## Supported Stacks
|
|
41
|
+
|
|
42
|
+
| Stack | Image | Status |
|
|
43
|
+
|-------|-------|--------|
|
|
44
|
+
| Laravel | `ghcr.io/reyemtech/laravel-upgrade-agent` | Available |
|
|
45
|
+
| React | — | Planned |
|
|
46
|
+
| Rails | — | Planned |
|
|
47
|
+
| Django | — | Planned |
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Config is persisted to `~/.stack-upgrade/config.json`:
|
|
52
|
+
|
|
53
|
+
- Claude credentials (auto-detected from `~/.claude/.credentials.json`, env vars, or manual input)
|
|
54
|
+
- GitHub token
|
|
55
|
+
- Preferred run target (Docker or Kubernetes)
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
[BSL 1.1](https://github.com/ReyemTech/stack-upgrade/blob/main/LICENSE)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
<p align="center">
|
|
64
|
+
<a href="https://www.reyem.tech">
|
|
65
|
+
<img src="https://www.reyem.tech/images/logo-light-tagline.webp" alt="ReyemTech" width="200">
|
|
66
|
+
</a>
|
|
67
|
+
</p>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reyemtech/stack-upgrade",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI to launch Stack Upgrade Agents via Docker or Kubernetes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/
|
|
21
|
+
"url": "git+https://github.com/reyemtech/stack-upgrade.git",
|
|
22
22
|
"directory": "cli"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT"
|
package/src/github.js
CHANGED
|
@@ -88,23 +88,32 @@ export async function discoverRepos() {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Prompt
|
|
92
|
-
* @param {Array} repos
|
|
91
|
+
* Prompt for a manual repo URL and return a repo object.
|
|
93
92
|
* @returns {Promise<{ name: string, url: string, stack: string, version: string }>}
|
|
94
93
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
94
|
+
async function promptManualUrl() {
|
|
95
|
+
const url = await p.text({
|
|
96
|
+
message: 'Enter the repository URL:',
|
|
97
|
+
placeholder: 'https://github.com/org/repo.git',
|
|
98
|
+
validate: (v) => {
|
|
99
|
+
if (!v) return 'URL is required';
|
|
100
|
+
if (!v.includes('github.com')) return 'Must be a GitHub URL';
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
if (p.isCancel(url)) process.exit(0);
|
|
104
|
+
|
|
105
|
+
return { name: url.replace(/.*github\.com[:/]/, '').replace(/\.git$/, ''), url, stack: 'laravel', version: 'unknown' };
|
|
106
|
+
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Prompt user to select one or more repos (or enter manually).
|
|
110
|
+
* @param {Array} repos
|
|
111
|
+
* @returns {Promise<Array<{ name: string, url: string, stack: string, version: string }>>}
|
|
112
|
+
*/
|
|
113
|
+
export async function selectRepos(repos) {
|
|
114
|
+
if (repos.length === 0) {
|
|
115
|
+
const repo = await promptManualUrl();
|
|
116
|
+
return [repo];
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
const options = repos.map((r) => ({
|
|
@@ -114,24 +123,22 @@ export async function selectRepo(repos) {
|
|
|
114
123
|
}));
|
|
115
124
|
options.push({ value: 'manual', label: 'Enter URL manually' });
|
|
116
125
|
|
|
117
|
-
const
|
|
118
|
-
message: 'Which
|
|
126
|
+
const choices = await p.multiselect({
|
|
127
|
+
message: 'Which repos do you want to upgrade?',
|
|
119
128
|
options,
|
|
129
|
+
required: true,
|
|
120
130
|
});
|
|
121
|
-
if (p.isCancel(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
if (p.isCancel(url)) process.exit(0);
|
|
132
|
-
|
|
133
|
-
return { name: url.replace(/.*github\.com[:/]/, '').replace(/\.git$/, ''), url, stack: 'laravel', version: 'unknown' };
|
|
131
|
+
if (p.isCancel(choices)) process.exit(0);
|
|
132
|
+
|
|
133
|
+
const selected = [];
|
|
134
|
+
for (const choice of choices) {
|
|
135
|
+
if (choice === 'manual') {
|
|
136
|
+
const repo = await promptManualUrl();
|
|
137
|
+
selected.push(repo);
|
|
138
|
+
} else {
|
|
139
|
+
selected.push(choice);
|
|
140
|
+
}
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
return
|
|
143
|
+
return selected;
|
|
137
144
|
}
|
package/src/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { getGhToken, getGhUser, discoverRepos,
|
|
5
|
+
import { getGhToken, getGhUser, discoverRepos, selectRepos } from './github.js';
|
|
6
6
|
import { detectClaudeCredentials } from './credentials.js';
|
|
7
|
-
import { askRunTarget, askTargetVersion, askPush, askSuffix
|
|
7
|
+
import { askRunTarget, askTargetVersion, askPush, askSuffix } from './prompts.js';
|
|
8
8
|
import { hasDocker, launchDocker } from './docker.js';
|
|
9
9
|
import { hasKubectl, launchKubernetes } from './kubectl.js';
|
|
10
10
|
import { getConfig, saveConfig } from './config.js';
|
|
@@ -71,22 +71,28 @@ async function main() {
|
|
|
71
71
|
const push = await askPush();
|
|
72
72
|
const suffix = await askSuffix();
|
|
73
73
|
|
|
74
|
-
// ---
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
// eslint-disable-next-line no-constant-condition
|
|
78
|
-
while (true) {
|
|
79
|
-
const repo = await selectRepo(repos);
|
|
74
|
+
// --- Repo selection ---
|
|
75
|
+
const selectedRepos = await selectRepos(repos);
|
|
80
76
|
|
|
81
|
-
|
|
77
|
+
// Resolve stacks for each repo
|
|
78
|
+
const repoStacks = selectedRepos.map((repo) => {
|
|
82
79
|
const stackConfig = getStack(repo.stack);
|
|
83
80
|
if (!stackConfig) {
|
|
84
81
|
p.log.warn(`Unknown stack: ${repo.stack}. Defaulting to Laravel.`);
|
|
85
82
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
return { repo, stack: stackConfig || getStack('laravel') };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// If all repos share the same stack, ask target version once; otherwise per-repo
|
|
87
|
+
const uniqueStacks = new Set(repoStacks.map((rs) => rs.repo.stack));
|
|
88
|
+
let sharedVersion = null;
|
|
89
|
+
if (uniqueStacks.size === 1) {
|
|
90
|
+
sharedVersion = await askTargetVersion(repoStacks[0].stack.versionLabel);
|
|
91
|
+
}
|
|
89
92
|
|
|
93
|
+
const upgrades = [];
|
|
94
|
+
for (const { repo, stack } of repoStacks) {
|
|
95
|
+
const targetVersion = sharedVersion || await askTargetVersion(`${stack.versionLabel} for ${repo.name}`);
|
|
90
96
|
const branchName = suffix
|
|
91
97
|
? `${stack.branchPrefix}-${targetVersion}-${suffix}`
|
|
92
98
|
: `${stack.branchPrefix}-${targetVersion}`;
|
|
@@ -105,20 +111,23 @@ async function main() {
|
|
|
105
111
|
envKey: stack.envKey,
|
|
106
112
|
branchName,
|
|
107
113
|
});
|
|
108
|
-
|
|
109
|
-
const another = await askAddAnother();
|
|
110
|
-
if (!another) break;
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
// --- Confirmation ---
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
);
|
|
117
|
+
const targetLabel = target === 'docker' ? 'Local Docker' : 'Kubernetes';
|
|
118
|
+
const maxName = Math.max(...upgrades.map((u) => u.repoName.length));
|
|
119
|
+
const maxStack = Math.max(...upgrades.map((u) => `${u.stackName} → ${u.targetVersion}`.length));
|
|
120
|
+
|
|
121
|
+
const summaryLines = upgrades.map((u, i) => {
|
|
122
|
+
const name = u.repoName.padEnd(maxName);
|
|
123
|
+
const stack = `${u.stackName} → ${u.targetVersion}`.padEnd(maxStack);
|
|
124
|
+
return ` ${i + 1}. ${name} ${stack} ${targetLabel}`;
|
|
125
|
+
});
|
|
117
126
|
|
|
118
127
|
p.note([
|
|
119
128
|
...summaryLines,
|
|
120
129
|
'',
|
|
121
|
-
`Push+PR: ${push ? 'yes' : 'no'}${suffix ? `
|
|
130
|
+
`Push+PR: ${push ? 'yes' : 'no'}${suffix ? ` Suffix: ${suffix}` : ''}`,
|
|
122
131
|
].join('\n'), `Ready to launch ${upgrades.length} ${upgrades.length === 1 ? 'upgrade' : 'upgrades'}`);
|
|
123
132
|
|
|
124
133
|
const confirm = await p.confirm({ message: 'Launch now?' });
|
package/src/prompts.js
CHANGED
|
@@ -70,16 +70,3 @@ export async function askRunTarget({ hasDocker, hasKubectl }) {
|
|
|
70
70
|
if (p.isCancel(target)) process.exit(0);
|
|
71
71
|
return target;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Prompt to add another upgrade to the queue.
|
|
76
|
-
* @returns {Promise<boolean>}
|
|
77
|
-
*/
|
|
78
|
-
export async function askAddAnother() {
|
|
79
|
-
const another = await p.confirm({
|
|
80
|
-
message: 'Add another upgrade?',
|
|
81
|
-
initialValue: false,
|
|
82
|
-
});
|
|
83
|
-
if (p.isCancel(another)) process.exit(0);
|
|
84
|
-
return another;
|
|
85
|
-
}
|