@reyemtech/stack-upgrade 0.4.0 → 0.4.7

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 ADDED
@@ -0,0 +1,67 @@
1
+ # @reyemtech/stack-upgrade
2
+
3
+ [![npm](https://img.shields.io/npm/v/@reyemtech/stack-upgrade)](https://www.npmjs.com/package/@reyemtech/stack-upgrade)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@reyemtech/stack-upgrade)](https://www.npmjs.com/package/@reyemtech/stack-upgrade)
5
+ [![License: BSL 1.1](https://img.shields.io/badge/License-BSL_1.1-yellow.svg)](https://github.com/ReyemTech/stack-upgrade/blob/main/LICENSE)
6
+ [![Node](https://img.shields.io/node/v/@reyemtech/stack-upgrade)](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.4.0",
3
+ "version": "0.4.7",
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/ReyemTech/stack-upgrade.git",
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 user to select a repo or enter manually.
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
- export async function selectRepo(repos) {
96
- if (repos.length === 0) {
97
- const url = await p.text({
98
- message: 'Enter the repository URL:',
99
- placeholder: 'https://github.com/org/repo.git',
100
- validate: (v) => {
101
- if (!v) return 'URL is required';
102
- if (!v.includes('github.com')) return 'Must be a GitHub URL';
103
- },
104
- });
105
- if (p.isCancel(url)) process.exit(0);
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
- return { name: url.replace(/.*github\.com[:/]/, '').replace(/\.git$/, ''), url, stack: 'laravel', version: 'unknown' };
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 choice = await p.select({
118
- message: 'Which repo do you want to upgrade?',
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(choice)) process.exit(0);
122
-
123
- if (choice === 'manual') {
124
- const url = await p.text({
125
- message: 'Enter the repository URL:',
126
- placeholder: 'https://github.com/org/repo.git',
127
- validate: (v) => {
128
- if (!v) return 'URL is required';
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 choice;
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, selectRepo } from './github.js';
5
+ import { getGhToken, getGhUser, discoverRepos, selectRepos } from './github.js';
6
6
  import { detectClaudeCredentials } from './credentials.js';
7
- import { askRunTarget, askTargetVersion, askPush, askSuffix, askAddAnother } from './prompts.js';
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
- // --- Multi-upgrade loop ---
75
- const upgrades = [];
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
- // Resolve stack
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
- const stack = stackConfig || getStack('laravel');
87
-
88
- const targetVersion = await askTargetVersion(stack.versionLabel);
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 summaryLines = upgrades.map((u, i) =>
115
- ` ${i + 1}. ${u.repoName} ${u.stackName} ${u.targetVersion} ${target === 'docker' ? 'Local Docker' : 'Kubernetes'}`,
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 ? ` Suffix: ${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
- }