outfitter 0.2.5 → 0.2.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 +60 -31
- package/dist/actions.d.ts +2 -0
- package/dist/actions.js +35 -0
- package/dist/cli.js +3 -2
- package/dist/commands/add.d.ts +54 -0
- package/dist/commands/add.js +16 -0
- package/dist/commands/check-tsdoc.d.ts +22 -0
- package/dist/commands/check-tsdoc.js +8 -0
- package/dist/commands/check.d.ts +91 -0
- package/dist/commands/check.js +14 -0
- package/dist/commands/demo.d.ts +21 -0
- package/dist/commands/demo.js +8 -0
- package/dist/commands/docs-module-loader.d.ts +2 -0
- package/dist/commands/docs-module-loader.js +8 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +25 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +32 -0
- package/dist/commands/repo.d.ts +3 -0
- package/dist/commands/repo.js +9 -0
- package/dist/commands/scaffold.d.ts +4 -0
- package/dist/commands/scaffold.js +32 -0
- package/dist/commands/shared-deps.d.ts +21 -0
- package/dist/commands/shared-deps.js +11 -0
- package/dist/commands/upgrade-codemods.d.ts +42 -0
- package/dist/commands/upgrade-codemods.js +15 -0
- package/dist/commands/upgrade-planner.d.ts +58 -0
- package/dist/commands/upgrade-planner.js +8 -0
- package/dist/commands/upgrade-workspace.d.ts +2 -0
- package/dist/commands/upgrade-workspace.js +16 -0
- package/dist/commands/upgrade.d.ts +221 -0
- package/dist/commands/upgrade.js +25 -0
- package/dist/create/index.d.ts +5 -0
- package/dist/create/index.js +30 -0
- package/dist/create/planner.d.ts +3 -0
- package/dist/create/planner.js +22 -0
- package/dist/create/presets.d.ts +3 -0
- package/dist/create/presets.js +12 -0
- package/dist/create/types.d.ts +2 -0
- package/dist/create/types.js +1 -0
- package/dist/engine/blocks.d.ts +3 -0
- package/dist/engine/blocks.js +12 -0
- package/dist/engine/collector.d.ts +2 -0
- package/dist/engine/collector.js +8 -0
- package/dist/engine/config.d.ts +3 -0
- package/dist/engine/config.js +15 -0
- package/dist/engine/dependency-versions.d.ts +12 -0
- package/dist/engine/dependency-versions.js +12 -0
- package/dist/engine/executor.d.ts +3 -0
- package/dist/engine/executor.js +19 -0
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.js +68 -0
- package/dist/engine/names.d.ts +2 -0
- package/dist/engine/names.js +24 -0
- package/dist/engine/post-scaffold.d.ts +3 -0
- package/dist/engine/post-scaffold.js +8 -0
- package/dist/engine/render-plan.d.ts +7 -0
- package/dist/engine/render-plan.js +9 -0
- package/dist/engine/template.d.ts +3 -0
- package/dist/engine/template.js +17 -0
- package/dist/engine/types.d.ts +2 -0
- package/dist/engine/types.js +8 -0
- package/dist/engine/workspace.d.ts +3 -0
- package/dist/engine/workspace.js +20 -0
- package/dist/index.d.ts +100 -129
- package/dist/index.js +1 -9
- package/dist/manifest.d.ts +71 -0
- package/dist/manifest.js +16 -0
- package/dist/output-mode.d.ts +2 -0
- package/dist/output-mode.js +10 -0
- package/dist/shared/{chunk-tpwtpa74.js → chunk-x6644tk8.js} +3840 -2881
- package/dist/shared/outfitter-109s75x0.d.ts +76 -0
- package/dist/shared/outfitter-1fy7byz5.js +170 -0
- package/dist/shared/outfitter-1h7k8xxt.js +29 -0
- package/dist/shared/outfitter-20f6a2n4.js +35 -0
- package/dist/shared/outfitter-344t1r38.js +1 -0
- package/dist/shared/outfitter-4q1zfmvc.js +154 -0
- package/dist/shared/outfitter-4s9meh3j.js +221 -0
- package/dist/shared/outfitter-5akzvppx.js +125 -0
- package/dist/shared/outfitter-5y646xzk.js +301 -0
- package/dist/shared/outfitter-5yjr404v.d.ts +22 -0
- package/dist/shared/outfitter-63gse8fv.js +316 -0
- package/dist/shared/outfitter-6bkqjk86.d.ts +3 -0
- package/dist/shared/outfitter-6fgk6adm.d.ts +40 -0
- package/dist/shared/outfitter-79vfxt6y.js +269 -0
- package/dist/shared/outfitter-7ch26yq8.js +885 -0
- package/dist/shared/outfitter-7r12fj7f.js +30 -0
- package/dist/shared/outfitter-8y2dfx6n.js +11 -0
- package/dist/shared/outfitter-9x1brcmq.js +184 -0
- package/dist/shared/outfitter-a79xrm12.d.ts +17 -0
- package/dist/shared/outfitter-amc4jbs1.d.ts +50 -0
- package/dist/shared/outfitter-bn9c8p2e.js +204 -0
- package/dist/shared/outfitter-bpr28y54.js +70 -0
- package/dist/shared/outfitter-dpj9erew.d.ts +4 -0
- package/dist/shared/outfitter-e9rrfekb.d.ts +51 -0
- package/dist/shared/outfitter-ehp18x1n.js +1 -0
- package/dist/shared/outfitter-f9znfhkn.d.ts +5 -0
- package/dist/shared/outfitter-fhnjpjwc.d.ts +18 -0
- package/dist/shared/outfitter-fn20r49x.d.ts +5 -0
- package/dist/shared/outfitter-h3q6ae6d.d.ts +48 -0
- package/dist/shared/outfitter-ha89qf8q.js +132 -0
- package/dist/shared/outfitter-hsp8vy5m.d.ts +146 -0
- package/dist/shared/outfitter-hvsaxgcp.js +1 -0
- package/dist/shared/outfitter-j833sxws.js +61 -0
- package/dist/shared/outfitter-ksyvwmb5.js +191 -0
- package/dist/shared/outfitter-m3ehh37q.d.ts +22 -0
- package/dist/shared/outfitter-m44n0qzw.js +161 -0
- package/dist/shared/outfitter-mdt37hqm.js +4 -0
- package/dist/shared/outfitter-mt7d1ek2.js +698 -0
- package/dist/shared/outfitter-mtbpabf3.js +91 -0
- package/dist/shared/outfitter-n9g1zk4x.d.ts +66 -0
- package/dist/shared/outfitter-p71qb0f0.js +82 -0
- package/dist/shared/outfitter-pcj9gg2g.js +909 -0
- package/dist/shared/outfitter-pj9vp00r.js +601 -0
- package/dist/shared/outfitter-qakwgrrh.d.ts +4 -0
- package/dist/shared/outfitter-r419zfgs.d.ts +30 -0
- package/dist/shared/outfitter-s7jetkge.d.ts +18 -0
- package/dist/shared/outfitter-ttjr95y9.js +98 -0
- package/dist/shared/outfitter-vh4xgb93.js +35 -0
- package/dist/shared/outfitter-w1j80j1r.js +326 -0
- package/dist/shared/outfitter-xe5mzgdc.js +208 -0
- package/dist/shared/outfitter-ybbazsxq.d.ts +14 -0
- package/dist/shared/outfitter-yraebrmw.d.ts +5 -0
- package/dist/shared/outfitter-yvksv5qb.js +322 -0
- package/dist/shared/outfitter-z0we32cp.d.ts +63 -0
- package/dist/shared/outfitter-z5sx06qe.d.ts +25 -0
- package/dist/shared/outfitter-zwyvewr1.js +36 -0
- package/dist/targets/index.d.ts +4 -0
- package/dist/targets/index.js +29 -0
- package/dist/targets/registry.d.ts +3 -0
- package/dist/targets/registry.js +28 -0
- package/dist/targets/types.d.ts +2 -0
- package/dist/targets/types.js +1 -0
- package/package.json +45 -28
- package/template-versions.json +22 -0
- package/templates/basic/package.json.template +6 -6
- package/templates/cli/biome.json.template +1 -1
- package/templates/cli/package.json.template +17 -9
- package/templates/daemon/biome.json.template +1 -1
- package/templates/daemon/package.json.template +18 -10
- package/templates/full-stack/.gitignore.template +30 -0
- package/templates/full-stack/README.md.template +30 -0
- package/templates/full-stack/apps/cli/package.json.template +39 -0
- package/templates/full-stack/apps/cli/src/cli.ts.template +24 -0
- package/templates/full-stack/apps/cli/src/index.test.ts.template +18 -0
- package/templates/full-stack/apps/cli/src/index.ts.template +5 -0
- package/templates/full-stack/apps/cli/tsconfig.json.template +37 -0
- package/templates/full-stack/apps/mcp/package.json.template +40 -0
- package/templates/full-stack/apps/mcp/src/index.test.ts.template +18 -0
- package/templates/full-stack/apps/mcp/src/index.ts.template +6 -0
- package/templates/full-stack/apps/mcp/src/mcp.ts.template +22 -0
- package/templates/full-stack/apps/mcp/src/server.ts.template +10 -0
- package/templates/full-stack/apps/mcp/tsconfig.json.template +37 -0
- package/templates/full-stack/package.json.template +16 -0
- package/templates/full-stack/packages/core/package.json.template +36 -0
- package/templates/full-stack/packages/core/src/handlers.ts.template +31 -0
- package/templates/full-stack/packages/core/src/index.test.ts.template +30 -0
- package/templates/full-stack/packages/core/src/index.ts.template +8 -0
- package/templates/full-stack/packages/core/src/types.ts.template +13 -0
- package/templates/full-stack/packages/core/tsconfig.json.template +34 -0
- package/templates/library/.gitignore.template +30 -0
- package/templates/library/README.md.template +29 -0
- package/templates/library/bunup.config.ts.template +20 -0
- package/templates/library/package.json.template +55 -0
- package/templates/library/src/handlers.ts.template +31 -0
- package/templates/library/src/index.test.ts.template +35 -0
- package/templates/library/src/index.ts.template +8 -0
- package/templates/library/src/types.ts.template +13 -0
- package/templates/library/tsconfig.json.template +34 -0
- package/templates/mcp/biome.json.template +1 -1
- package/templates/mcp/package.json.template +17 -9
- package/templates/mcp/src/index.ts.template +1 -1
- package/templates/mcp/src/mcp.ts.template +28 -74
- package/templates/mcp/src/server.ts.template +2 -9
- package/templates/minimal/package.json.template +13 -6
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
analyzeUpgrades
|
|
4
|
+
} from "./outfitter-bpr28y54.js";
|
|
5
|
+
import {
|
|
6
|
+
applyUpdatesToWorkspace,
|
|
7
|
+
getInstalledPackagesFromWorkspace,
|
|
8
|
+
runInstall
|
|
9
|
+
} from "./outfitter-yvksv5qb.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveStructuredOutputMode
|
|
12
|
+
} from "./outfitter-7r12fj7f.js";
|
|
13
|
+
import {
|
|
14
|
+
__require
|
|
15
|
+
} from "./outfitter-mdt37hqm.js";
|
|
16
|
+
|
|
17
|
+
// apps/outfitter/src/commands/upgrade-codemods.ts
|
|
18
|
+
import { existsSync as existsSync2 } from "fs";
|
|
19
|
+
import { isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
|
|
20
|
+
import { InternalError as InternalError2, Result as Result2 } from "@outfitter/contracts";
|
|
21
|
+
|
|
22
|
+
// apps/outfitter/src/commands/upgrade.ts
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
24
|
+
import { join, resolve } from "path";
|
|
25
|
+
import { output } from "@outfitter/cli";
|
|
26
|
+
import { InternalError, Result } from "@outfitter/contracts";
|
|
27
|
+
import { createTheme } from "@outfitter/tui/render";
|
|
28
|
+
var FRONTMATTER_BLOCK_REGEX = /^---\r?\n[\s\S]*?\r?\n---\r?\n*/;
|
|
29
|
+
async function getLatestVersion(name) {
|
|
30
|
+
try {
|
|
31
|
+
const proc = Bun.spawn(["npm", "view", name, "version"], {
|
|
32
|
+
stdout: "pipe",
|
|
33
|
+
stderr: "pipe"
|
|
34
|
+
});
|
|
35
|
+
const stdout = await new Response(proc.stdout).text();
|
|
36
|
+
const exitCode = await proc.exited;
|
|
37
|
+
if (exitCode !== 0)
|
|
38
|
+
return null;
|
|
39
|
+
return stdout.trim() || null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
var MIGRATION_DOC_PATHS = ["plugins/outfitter/shared/migrations"];
|
|
45
|
+
function findMigrationDocsDir(cwd, binaryDir) {
|
|
46
|
+
for (const relative of MIGRATION_DOC_PATHS) {
|
|
47
|
+
const dir = join(cwd, relative);
|
|
48
|
+
if (existsSync(dir))
|
|
49
|
+
return dir;
|
|
50
|
+
}
|
|
51
|
+
let current = resolve(cwd);
|
|
52
|
+
const root = resolve("/");
|
|
53
|
+
while (current !== root) {
|
|
54
|
+
const parent = resolve(current, "..");
|
|
55
|
+
if (parent === current)
|
|
56
|
+
break;
|
|
57
|
+
current = parent;
|
|
58
|
+
for (const relative of MIGRATION_DOC_PATHS) {
|
|
59
|
+
const dir = join(current, relative);
|
|
60
|
+
if (existsSync(dir))
|
|
61
|
+
return dir;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const resolvedBinaryDir = binaryDir ?? resolve(import.meta.dir, "../../../..");
|
|
65
|
+
for (const relative of MIGRATION_DOC_PATHS) {
|
|
66
|
+
const dir = join(resolvedBinaryDir, relative);
|
|
67
|
+
if (existsSync(dir))
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function readMigrationDocs(migrationsDir, shortName, fromVersion, toVersion) {
|
|
73
|
+
const glob = new Bun.Glob(`outfitter-${shortName}-*.md`);
|
|
74
|
+
const versionPattern = new RegExp(`^outfitter-${shortName}-(\\d+\\.\\d+\\.\\d+)\\.md$`);
|
|
75
|
+
const docs = [];
|
|
76
|
+
for (const entry of glob.scanSync({ cwd: migrationsDir })) {
|
|
77
|
+
const match = entry.match(versionPattern);
|
|
78
|
+
if (!match?.[1])
|
|
79
|
+
continue;
|
|
80
|
+
const docVersion = match[1];
|
|
81
|
+
if (Bun.semver.order(docVersion, fromVersion) <= 0)
|
|
82
|
+
continue;
|
|
83
|
+
if (Bun.semver.order(docVersion, toVersion) > 0)
|
|
84
|
+
continue;
|
|
85
|
+
const filePath = join(migrationsDir, entry);
|
|
86
|
+
let content;
|
|
87
|
+
try {
|
|
88
|
+
content = readFileSync(filePath, "utf-8");
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const body = content.replace(FRONTMATTER_BLOCK_REGEX, "").trim();
|
|
93
|
+
if (body) {
|
|
94
|
+
docs.push({ version: docVersion, content: body });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
docs.sort((a, b) => Bun.semver.order(a.version, b.version));
|
|
98
|
+
return docs.map((d) => d.content);
|
|
99
|
+
}
|
|
100
|
+
function readMigrationBreakingFlag(migrationsDir, shortName, version) {
|
|
101
|
+
const filePath = join(migrationsDir, `outfitter-${shortName}-${version}.md`);
|
|
102
|
+
if (!existsSync(filePath)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let content;
|
|
106
|
+
try {
|
|
107
|
+
content = readFileSync(filePath, "utf-8");
|
|
108
|
+
} catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
112
|
+
if (!frontmatter?.[1]) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const breakingLine = frontmatter[1].split(/\r?\n/).find((line) => line.trimStart().startsWith("breaking:"));
|
|
116
|
+
if (breakingLine === undefined) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const rawValue = breakingLine.split(":").slice(1).join(":").trim();
|
|
120
|
+
if (rawValue === "true")
|
|
121
|
+
return true;
|
|
122
|
+
if (rawValue === "false")
|
|
123
|
+
return false;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
var VALID_CHANGE_TYPES = new Set([
|
|
127
|
+
"renamed",
|
|
128
|
+
"removed",
|
|
129
|
+
"signature-changed",
|
|
130
|
+
"moved",
|
|
131
|
+
"deprecated",
|
|
132
|
+
"added"
|
|
133
|
+
]);
|
|
134
|
+
function parseYamlValue(raw) {
|
|
135
|
+
const trimmed = raw.trim();
|
|
136
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
137
|
+
return trimmed.slice(1, -1);
|
|
138
|
+
}
|
|
139
|
+
return trimmed;
|
|
140
|
+
}
|
|
141
|
+
function parseMigrationFrontmatter(content) {
|
|
142
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
143
|
+
if (!fmMatch?.[1])
|
|
144
|
+
return null;
|
|
145
|
+
const fmBlock = fmMatch[1];
|
|
146
|
+
const lines = fmBlock.split(/\r?\n/);
|
|
147
|
+
let pkg;
|
|
148
|
+
let version;
|
|
149
|
+
let breaking;
|
|
150
|
+
let changesStartIdx = -1;
|
|
151
|
+
for (let i = 0;i < lines.length; i++) {
|
|
152
|
+
const line = lines[i];
|
|
153
|
+
if (line === undefined)
|
|
154
|
+
continue;
|
|
155
|
+
const trimmed = line.trimStart();
|
|
156
|
+
if (trimmed.startsWith("package:")) {
|
|
157
|
+
pkg = parseYamlValue(trimmed.slice("package:".length));
|
|
158
|
+
} else if (trimmed.startsWith("version:")) {
|
|
159
|
+
version = parseYamlValue(trimmed.slice("version:".length));
|
|
160
|
+
} else if (trimmed.startsWith("breaking:")) {
|
|
161
|
+
const val = parseYamlValue(trimmed.slice("breaking:".length));
|
|
162
|
+
if (val === "true")
|
|
163
|
+
breaking = true;
|
|
164
|
+
else if (val === "false")
|
|
165
|
+
breaking = false;
|
|
166
|
+
} else if (trimmed.startsWith("changes:")) {
|
|
167
|
+
changesStartIdx = i + 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (pkg === undefined || version === undefined || breaking === undefined) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
let changes;
|
|
174
|
+
if (changesStartIdx >= 0) {
|
|
175
|
+
changes = parseChangesArray(lines, changesStartIdx);
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
package: pkg,
|
|
179
|
+
version,
|
|
180
|
+
breaking,
|
|
181
|
+
...changes !== undefined ? { changes } : {}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function parseChangesArray(lines, startIdx) {
|
|
185
|
+
const changes = [];
|
|
186
|
+
let current = null;
|
|
187
|
+
for (let i = startIdx;i < lines.length; i++) {
|
|
188
|
+
const line = lines[i];
|
|
189
|
+
if (line === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
if (/^\s+-\s+/.test(line)) {
|
|
192
|
+
if (current !== null) {
|
|
193
|
+
const change = buildChange(current);
|
|
194
|
+
if (change)
|
|
195
|
+
changes.push(change);
|
|
196
|
+
}
|
|
197
|
+
current = {};
|
|
198
|
+
const afterDash = line.replace(/^\s+-\s+/, "");
|
|
199
|
+
const colonIdx = afterDash.indexOf(":");
|
|
200
|
+
if (colonIdx >= 0) {
|
|
201
|
+
const key = afterDash.slice(0, colonIdx).trim();
|
|
202
|
+
const val = parseYamlValue(afterDash.slice(colonIdx + 1));
|
|
203
|
+
current[key] = val;
|
|
204
|
+
}
|
|
205
|
+
} else if (current !== null && /^\s{4,}\S/.test(line)) {
|
|
206
|
+
const trimmed = line.trim();
|
|
207
|
+
const colonIdx = trimmed.indexOf(":");
|
|
208
|
+
if (colonIdx >= 0) {
|
|
209
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
210
|
+
const val = parseYamlValue(trimmed.slice(colonIdx + 1));
|
|
211
|
+
current[key] = val;
|
|
212
|
+
}
|
|
213
|
+
} else if (/^\S/.test(line)) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (current !== null) {
|
|
218
|
+
const change = buildChange(current);
|
|
219
|
+
if (change)
|
|
220
|
+
changes.push(change);
|
|
221
|
+
}
|
|
222
|
+
return changes;
|
|
223
|
+
}
|
|
224
|
+
function buildChange(raw) {
|
|
225
|
+
const type = raw["type"];
|
|
226
|
+
if (!(type && VALID_CHANGE_TYPES.has(type))) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
type,
|
|
231
|
+
...raw["from"] ? { from: raw["from"] } : {},
|
|
232
|
+
...raw["to"] ? { to: raw["to"] } : {},
|
|
233
|
+
...raw["path"] ? { path: raw["path"] } : {},
|
|
234
|
+
...raw["export"] ? { export: raw["export"] } : {},
|
|
235
|
+
...raw["detail"] ? { detail: raw["detail"] } : {},
|
|
236
|
+
...raw["codemod"] ? { codemod: raw["codemod"] } : {}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function readMigrationDocsWithMetadata(migrationsDir, shortName, fromVersion, toVersion) {
|
|
240
|
+
const glob = new Bun.Glob(`outfitter-${shortName}-*.md`);
|
|
241
|
+
const versionPattern = new RegExp(`^outfitter-${shortName}-(\\d+\\.\\d+\\.\\d+)\\.md$`);
|
|
242
|
+
const docs = [];
|
|
243
|
+
for (const entry of glob.scanSync({ cwd: migrationsDir })) {
|
|
244
|
+
const match = entry.match(versionPattern);
|
|
245
|
+
if (!match?.[1])
|
|
246
|
+
continue;
|
|
247
|
+
const docVersion = match[1];
|
|
248
|
+
if (Bun.semver.order(docVersion, fromVersion) <= 0)
|
|
249
|
+
continue;
|
|
250
|
+
if (Bun.semver.order(docVersion, toVersion) > 0)
|
|
251
|
+
continue;
|
|
252
|
+
const filePath = join(migrationsDir, entry);
|
|
253
|
+
let content;
|
|
254
|
+
try {
|
|
255
|
+
content = readFileSync(filePath, "utf-8");
|
|
256
|
+
} catch {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const frontmatter = parseMigrationFrontmatter(content);
|
|
260
|
+
if (!frontmatter)
|
|
261
|
+
continue;
|
|
262
|
+
const body = content.replace(FRONTMATTER_BLOCK_REGEX, "").trim();
|
|
263
|
+
docs.push({ frontmatter, body, version: docVersion });
|
|
264
|
+
}
|
|
265
|
+
docs.sort((a, b) => Bun.semver.order(a.version, b.version));
|
|
266
|
+
return docs;
|
|
267
|
+
}
|
|
268
|
+
function buildMigrationGuides(packages, migrationsDir) {
|
|
269
|
+
const guides = [];
|
|
270
|
+
for (const pkg of packages) {
|
|
271
|
+
if (!pkg.updateAvailable || pkg.latest === null)
|
|
272
|
+
continue;
|
|
273
|
+
let steps = [];
|
|
274
|
+
let allChanges;
|
|
275
|
+
if (migrationsDir !== null) {
|
|
276
|
+
const shortName = pkg.name.replace("@outfitter/", "");
|
|
277
|
+
const metaDocs = readMigrationDocsWithMetadata(migrationsDir, shortName, pkg.current, pkg.latest);
|
|
278
|
+
steps = metaDocs.map((doc) => doc.body);
|
|
279
|
+
const changes = [];
|
|
280
|
+
for (const doc of metaDocs) {
|
|
281
|
+
if (doc.frontmatter.changes) {
|
|
282
|
+
changes.push(...doc.frontmatter.changes);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (changes.length > 0) {
|
|
286
|
+
allChanges = changes;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
guides.push({
|
|
290
|
+
packageName: pkg.name,
|
|
291
|
+
fromVersion: pkg.current,
|
|
292
|
+
toVersion: pkg.latest,
|
|
293
|
+
breaking: pkg.breaking,
|
|
294
|
+
steps,
|
|
295
|
+
...allChanges !== undefined ? { changes: allChanges } : {}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return guides;
|
|
299
|
+
}
|
|
300
|
+
function getVersionPrefix(specifier) {
|
|
301
|
+
if (specifier.startsWith("workspace:")) {
|
|
302
|
+
const inner = specifier.slice("workspace:".length);
|
|
303
|
+
return `workspace:${getVersionPrefix(inner)}`;
|
|
304
|
+
}
|
|
305
|
+
const match = specifier.match(/^([\^~>=<]+)/);
|
|
306
|
+
return match?.[1] ?? "";
|
|
307
|
+
}
|
|
308
|
+
async function applyUpdates(cwd, updates) {
|
|
309
|
+
const pkgPath = join(cwd, "package.json");
|
|
310
|
+
let raw;
|
|
311
|
+
try {
|
|
312
|
+
raw = readFileSync(pkgPath, "utf-8");
|
|
313
|
+
} catch {
|
|
314
|
+
return Result.err(InternalError.create("Failed to read package.json for apply", { cwd }));
|
|
315
|
+
}
|
|
316
|
+
let pkg;
|
|
317
|
+
try {
|
|
318
|
+
pkg = JSON.parse(raw);
|
|
319
|
+
} catch {
|
|
320
|
+
return Result.err(InternalError.create("Invalid JSON in package.json", { cwd }));
|
|
321
|
+
}
|
|
322
|
+
const updateMap = new Map;
|
|
323
|
+
for (const u of updates) {
|
|
324
|
+
updateMap.set(u.name, u.latestVersion);
|
|
325
|
+
}
|
|
326
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
327
|
+
const deps = pkg[section];
|
|
328
|
+
if (!deps)
|
|
329
|
+
continue;
|
|
330
|
+
for (const name of Object.keys(deps)) {
|
|
331
|
+
const newVersion = updateMap.get(name);
|
|
332
|
+
if (newVersion === undefined)
|
|
333
|
+
continue;
|
|
334
|
+
const currentSpecifier = deps[name];
|
|
335
|
+
if (currentSpecifier === undefined)
|
|
336
|
+
continue;
|
|
337
|
+
const prefix = getVersionPrefix(currentSpecifier);
|
|
338
|
+
deps[name] = `${prefix}${newVersion}`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const updated = `${JSON.stringify(pkg, null, 2)}
|
|
343
|
+
`;
|
|
344
|
+
await Bun.write(pkgPath, updated);
|
|
345
|
+
} catch {
|
|
346
|
+
return Result.err(InternalError.create("Failed to write updated package.json", { cwd }));
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const proc = Bun.spawn(["bun", "install"], {
|
|
350
|
+
cwd,
|
|
351
|
+
stdout: "pipe",
|
|
352
|
+
stderr: "pipe"
|
|
353
|
+
});
|
|
354
|
+
const exitCode = await proc.exited;
|
|
355
|
+
if (exitCode !== 0) {
|
|
356
|
+
const stderr = await new Response(proc.stderr).text();
|
|
357
|
+
return Result.err(InternalError.create("bun install failed", {
|
|
358
|
+
cwd,
|
|
359
|
+
exitCode,
|
|
360
|
+
stderr: stderr.trim()
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
return Result.err(InternalError.create("Failed to run bun install", { cwd }));
|
|
365
|
+
}
|
|
366
|
+
return Result.ok(undefined);
|
|
367
|
+
}
|
|
368
|
+
async function runUpgrade(options) {
|
|
369
|
+
const cwd = resolve(options.cwd);
|
|
370
|
+
const startedAt = new Date;
|
|
371
|
+
let workspaceRoot = null;
|
|
372
|
+
const emptyResult = {
|
|
373
|
+
packages: [],
|
|
374
|
+
total: 0,
|
|
375
|
+
updatesAvailable: 0,
|
|
376
|
+
hasBreaking: false,
|
|
377
|
+
applied: false,
|
|
378
|
+
appliedPackages: [],
|
|
379
|
+
skippedBreaking: []
|
|
380
|
+
};
|
|
381
|
+
const writeReport = (status, result, error) => {
|
|
382
|
+
writeUpgradeReportSafely(cwd, result, {
|
|
383
|
+
status,
|
|
384
|
+
startedAt,
|
|
385
|
+
workspaceRoot,
|
|
386
|
+
options,
|
|
387
|
+
...error !== undefined ? { error } : {}
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
try {
|
|
391
|
+
const migrationsDir = findMigrationDocsDir(cwd);
|
|
392
|
+
const migrationFlagsDir = findMigrationDocsDir(cwd, cwd);
|
|
393
|
+
const scanResult = getInstalledPackagesFromWorkspace(cwd);
|
|
394
|
+
if (scanResult.isErr()) {
|
|
395
|
+
writeReport("failed", emptyResult, scanResult.error);
|
|
396
|
+
return scanResult;
|
|
397
|
+
}
|
|
398
|
+
const scan = scanResult.value;
|
|
399
|
+
workspaceRoot = scan.workspaceRoot;
|
|
400
|
+
const requestedPackages = options.guidePackages;
|
|
401
|
+
let installed = scan.packages;
|
|
402
|
+
let unknownPackages;
|
|
403
|
+
if (requestedPackages && requestedPackages.length > 0) {
|
|
404
|
+
const filterSet = new Set(requestedPackages.map((p) => p.startsWith("@") ? p : `@outfitter/${p}`));
|
|
405
|
+
const found = new Set;
|
|
406
|
+
installed = scan.packages.filter((pkg) => {
|
|
407
|
+
if (filterSet.has(pkg.name)) {
|
|
408
|
+
found.add(pkg.name);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
});
|
|
413
|
+
const notFound = [...filterSet].filter((name) => !found.has(name));
|
|
414
|
+
if (notFound.length > 0) {
|
|
415
|
+
unknownPackages = notFound;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const installRoot = scan.workspaceRoot ?? cwd;
|
|
419
|
+
const codemodTargetDir = scan.workspaceRoot ?? cwd;
|
|
420
|
+
if (installed.length === 0 && !unknownPackages?.length) {
|
|
421
|
+
const result = {
|
|
422
|
+
...emptyResult,
|
|
423
|
+
...scan.conflicts.length > 0 ? { conflicts: scan.conflicts } : {}
|
|
424
|
+
};
|
|
425
|
+
writeReport("no_updates", result);
|
|
426
|
+
return Result.ok(result);
|
|
427
|
+
}
|
|
428
|
+
if (installed.length === 0 && unknownPackages?.length) {
|
|
429
|
+
const result = {
|
|
430
|
+
...emptyResult,
|
|
431
|
+
unknownPackages,
|
|
432
|
+
...scan.conflicts.length > 0 ? { conflicts: scan.conflicts } : {}
|
|
433
|
+
};
|
|
434
|
+
writeReport("no_updates", result);
|
|
435
|
+
return Result.ok(result);
|
|
436
|
+
}
|
|
437
|
+
const latestVersions = new Map;
|
|
438
|
+
const installedMap = new Map;
|
|
439
|
+
const npmFailures = new Set;
|
|
440
|
+
await Promise.all(installed.map(async (pkg) => {
|
|
441
|
+
installedMap.set(pkg.name, pkg.version);
|
|
442
|
+
const latest = await getLatestVersion(pkg.name);
|
|
443
|
+
if (latest !== null) {
|
|
444
|
+
const shortName = pkg.name.replace("@outfitter/", "");
|
|
445
|
+
const docBreaking = migrationFlagsDir !== null ? readMigrationBreakingFlag(migrationFlagsDir, shortName, latest) : undefined;
|
|
446
|
+
latestVersions.set(pkg.name, {
|
|
447
|
+
version: latest,
|
|
448
|
+
...docBreaking !== undefined ? { breaking: docBreaking } : {}
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
npmFailures.add(pkg.name);
|
|
452
|
+
}
|
|
453
|
+
}));
|
|
454
|
+
const plan = analyzeUpgrades(installedMap, latestVersions);
|
|
455
|
+
const packages = plan.packages.map((action) => ({
|
|
456
|
+
name: action.name,
|
|
457
|
+
current: action.currentVersion,
|
|
458
|
+
latest: npmFailures.has(action.name) ? null : action.latestVersion,
|
|
459
|
+
updateAvailable: action.classification === "upgradableNonBreaking" || action.classification === "upgradableBreaking",
|
|
460
|
+
breaking: action.breaking
|
|
461
|
+
}));
|
|
462
|
+
const updatesAvailable = packages.filter((p) => p.updateAvailable).length;
|
|
463
|
+
const hasBreaking = packages.some((p) => p.breaking);
|
|
464
|
+
const nonBreakingUpgradable = plan.packages.filter((a) => a.classification === "upgradableNonBreaking");
|
|
465
|
+
const breakingUpgradable = plan.packages.filter((a) => a.classification === "upgradableBreaking");
|
|
466
|
+
const includeBreaking = options.all === true;
|
|
467
|
+
const packagesToApply = includeBreaking ? [...nonBreakingUpgradable, ...breakingUpgradable] : nonBreakingUpgradable;
|
|
468
|
+
const skippedBreaking = includeBreaking ? [] : breakingUpgradable.map((a) => a.name);
|
|
469
|
+
const guidesData = options.guide === true ? buildMigrationGuides(packages, migrationsDir) : undefined;
|
|
470
|
+
const buildResult = (overrides = {}) => ({
|
|
471
|
+
packages,
|
|
472
|
+
total: packages.length,
|
|
473
|
+
updatesAvailable,
|
|
474
|
+
hasBreaking,
|
|
475
|
+
applied: false,
|
|
476
|
+
appliedPackages: [],
|
|
477
|
+
skippedBreaking,
|
|
478
|
+
...guidesData !== undefined ? { guides: guidesData } : {},
|
|
479
|
+
...unknownPackages !== undefined ? { unknownPackages } : {},
|
|
480
|
+
...scan.conflicts.length > 0 ? { conflicts: scan.conflicts } : {},
|
|
481
|
+
...overrides
|
|
482
|
+
});
|
|
483
|
+
if (options.dryRun) {
|
|
484
|
+
const result = buildResult();
|
|
485
|
+
writeReport("dry_run", result);
|
|
486
|
+
return Result.ok(result);
|
|
487
|
+
}
|
|
488
|
+
if (packagesToApply.length === 0) {
|
|
489
|
+
const result = buildResult();
|
|
490
|
+
writeReport("no_updates", result);
|
|
491
|
+
return Result.ok(result);
|
|
492
|
+
}
|
|
493
|
+
if (options.yes !== true && options.interactive !== false) {
|
|
494
|
+
const { confirmDestructive } = await import("@outfitter/tui/confirm");
|
|
495
|
+
const confirmed = await confirmDestructive({
|
|
496
|
+
message: `Apply ${packagesToApply.length} upgrade(s)?`,
|
|
497
|
+
itemCount: packagesToApply.length,
|
|
498
|
+
bypassFlag: false
|
|
499
|
+
});
|
|
500
|
+
if (confirmed.isErr()) {
|
|
501
|
+
const result = buildResult();
|
|
502
|
+
writeReport("cancelled", result);
|
|
503
|
+
return Result.ok(result);
|
|
504
|
+
}
|
|
505
|
+
} else if (options.interactive === false && options.yes !== true) {
|
|
506
|
+
const result = buildResult();
|
|
507
|
+
writeReport("skipped_non_interactive", result);
|
|
508
|
+
return Result.ok(result);
|
|
509
|
+
}
|
|
510
|
+
let applied = false;
|
|
511
|
+
const appliedPackages = [];
|
|
512
|
+
if (packagesToApply.length > 0) {
|
|
513
|
+
if (scan.workspaceRoot !== null) {
|
|
514
|
+
const applyResult = await applyUpdatesToWorkspace(scan.manifestPaths, scan.manifestsByPackage, packagesToApply);
|
|
515
|
+
if (applyResult.isErr()) {
|
|
516
|
+
const failureResult = buildResult();
|
|
517
|
+
writeReport("failed", failureResult, applyResult.error);
|
|
518
|
+
return applyResult;
|
|
519
|
+
}
|
|
520
|
+
const installResult = await runInstall(installRoot);
|
|
521
|
+
if (installResult.isErr()) {
|
|
522
|
+
const failureResult = buildResult();
|
|
523
|
+
writeReport("failed", failureResult, installResult.error);
|
|
524
|
+
return installResult;
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
const applyResult = await applyUpdates(cwd, packagesToApply);
|
|
528
|
+
if (applyResult.isErr()) {
|
|
529
|
+
const failureResult = buildResult();
|
|
530
|
+
writeReport("failed", failureResult, applyResult.error);
|
|
531
|
+
return applyResult;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
applied = true;
|
|
535
|
+
appliedPackages.push(...packagesToApply.map((a) => a.name));
|
|
536
|
+
}
|
|
537
|
+
let codemodSummary;
|
|
538
|
+
if (applied && options.noCodemods !== true && migrationsDir !== null) {
|
|
539
|
+
const codemodsDir = findCodemodsDir(cwd);
|
|
540
|
+
if (codemodsDir !== null) {
|
|
541
|
+
const allChangedFiles = [];
|
|
542
|
+
const allErrors = [];
|
|
543
|
+
let codemodCount = 0;
|
|
544
|
+
for (const pkg of packagesToApply) {
|
|
545
|
+
const shortName = pkg.name.replace("@outfitter/", "");
|
|
546
|
+
const codemods = discoverCodemods(migrationsDir, codemodsDir, shortName, installedMap.get(pkg.name) ?? "0.0.0", pkg.latestVersion);
|
|
547
|
+
for (const codemod of codemods) {
|
|
548
|
+
const codemodResult = await runCodemod(codemod.absolutePath, codemodTargetDir, false);
|
|
549
|
+
codemodCount++;
|
|
550
|
+
if (codemodResult.isOk()) {
|
|
551
|
+
allChangedFiles.push(...codemodResult.value.changedFiles);
|
|
552
|
+
allErrors.push(...codemodResult.value.errors);
|
|
553
|
+
} else {
|
|
554
|
+
allErrors.push(codemodResult.error.message);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (codemodCount > 0) {
|
|
559
|
+
codemodSummary = {
|
|
560
|
+
codemodCount,
|
|
561
|
+
changedFiles: allChangedFiles,
|
|
562
|
+
errors: allErrors
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const finalResult = buildResult({
|
|
568
|
+
applied,
|
|
569
|
+
appliedPackages,
|
|
570
|
+
...codemodSummary !== undefined ? { codemods: codemodSummary } : {}
|
|
571
|
+
});
|
|
572
|
+
writeReport("applied", finalResult);
|
|
573
|
+
return Result.ok(finalResult);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
const normalizedError = error && typeof error === "object" && "category" in error && "message" in error ? error : InternalError.create("Unexpected error in outfitter upgrade", {
|
|
576
|
+
cwd,
|
|
577
|
+
error: error instanceof Error ? error.message : String(error)
|
|
578
|
+
});
|
|
579
|
+
writeReport("failed", emptyResult, normalizedError);
|
|
580
|
+
return Result.err(normalizedError);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function printUpgradeResults(result, options) {
|
|
584
|
+
const structuredMode = resolveStructuredOutputMode(options?.mode);
|
|
585
|
+
if (structuredMode) {
|
|
586
|
+
await output(result, { mode: structuredMode });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const theme = createTheme();
|
|
590
|
+
const lines = ["", "Outfitter Upgrade", "", "=".repeat(60)];
|
|
591
|
+
if (result.packages.length === 0) {
|
|
592
|
+
lines.push("No @outfitter/* packages found in package.json.");
|
|
593
|
+
if (!result.unknownPackages || result.unknownPackages.length === 0) {
|
|
594
|
+
await output(lines, { mode: "human" });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
lines.push("");
|
|
598
|
+
} else {
|
|
599
|
+
lines.push(` ${"Package".padEnd(28)} ${"Current".padEnd(10)} ${"Available".padEnd(10)} Migration`);
|
|
600
|
+
lines.push(` ${"\u2500".repeat(28)} ${"\u2500".repeat(10)} ${"\u2500".repeat(10)} ${"\u2500".repeat(20)}`);
|
|
601
|
+
for (const pkg of result.packages) {
|
|
602
|
+
const name = pkg.name.padEnd(28);
|
|
603
|
+
const current = pkg.current.padEnd(10);
|
|
604
|
+
const available = (pkg.latest ?? "unknown").padEnd(10);
|
|
605
|
+
let migration;
|
|
606
|
+
if (pkg.latest === null) {
|
|
607
|
+
migration = theme.muted("lookup failed");
|
|
608
|
+
} else if (!pkg.updateAvailable) {
|
|
609
|
+
migration = theme.muted("up to date");
|
|
610
|
+
} else if (pkg.breaking) {
|
|
611
|
+
migration = theme.error("breaking");
|
|
612
|
+
} else {
|
|
613
|
+
migration = theme.success("non-breaking");
|
|
614
|
+
}
|
|
615
|
+
lines.push(` ${name} ${current} ${available} ${migration}`);
|
|
616
|
+
}
|
|
617
|
+
lines.push("");
|
|
618
|
+
}
|
|
619
|
+
if (result.applied && result.appliedPackages.length > 0) {
|
|
620
|
+
const breakingApplied = result.appliedPackages.filter((name) => result.packages.some((p) => p.name === name && p.breaking));
|
|
621
|
+
const nonBreakingApplied = result.appliedPackages.filter((name) => !result.packages.some((p) => p.name === name && p.breaking));
|
|
622
|
+
if (nonBreakingApplied.length > 0) {
|
|
623
|
+
lines.push(theme.success(`Applied ${nonBreakingApplied.length} non-breaking upgrade(s):`));
|
|
624
|
+
for (const name of nonBreakingApplied) {
|
|
625
|
+
lines.push(` - ${name}`);
|
|
626
|
+
}
|
|
627
|
+
lines.push("");
|
|
628
|
+
}
|
|
629
|
+
if (breakingApplied.length > 0) {
|
|
630
|
+
lines.push(theme.error(`Applied ${breakingApplied.length} breaking upgrade(s):`));
|
|
631
|
+
for (const name of breakingApplied) {
|
|
632
|
+
const pkg = result.packages.find((p) => p.name === name);
|
|
633
|
+
lines.push(` - ${name} (${pkg?.current} -> ${pkg?.latest})`);
|
|
634
|
+
}
|
|
635
|
+
lines.push("", theme.muted("Review migration guides: 'outfitter upgrade --guide'"));
|
|
636
|
+
lines.push("");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (result.skippedBreaking.length > 0 && options?.all !== true) {
|
|
640
|
+
if (result.applied) {
|
|
641
|
+
lines.push(theme.error(`Skipped ${result.skippedBreaking.length} breaking upgrade(s):`));
|
|
642
|
+
} else {
|
|
643
|
+
lines.push(" Excluded (breaking):");
|
|
644
|
+
}
|
|
645
|
+
for (const name of result.skippedBreaking) {
|
|
646
|
+
const pkg = result.packages.find((p) => p.name === name);
|
|
647
|
+
const codemodHint = pkg?.breaking ? "(migration guide)" : "";
|
|
648
|
+
lines.push(` ${name.padEnd(24)} ${(pkg?.current ?? "").padEnd(8)} -> ${(pkg?.latest ?? "").padEnd(8)} ${codemodHint}`.trimEnd());
|
|
649
|
+
}
|
|
650
|
+
lines.push("", theme.muted(" Use --all to include breaking changes"));
|
|
651
|
+
lines.push("");
|
|
652
|
+
}
|
|
653
|
+
if (result.codemods !== undefined) {
|
|
654
|
+
const uniqueChangedFiles = [
|
|
655
|
+
...new Set(result.codemods.changedFiles)
|
|
656
|
+
].sort();
|
|
657
|
+
lines.push(theme.info(`Ran ${result.codemods.codemodCount} codemod(s).`));
|
|
658
|
+
if (uniqueChangedFiles.length > 0) {
|
|
659
|
+
lines.push(theme.success(`Codemods changed ${uniqueChangedFiles.length} file(s):`));
|
|
660
|
+
for (const file of uniqueChangedFiles) {
|
|
661
|
+
lines.push(` - ${file}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (result.codemods.errors.length > 0) {
|
|
665
|
+
lines.push(theme.error(`Codemod errors (${result.codemods.errors.length}):`));
|
|
666
|
+
for (const error of result.codemods.errors) {
|
|
667
|
+
lines.push(` - ${error}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
lines.push("");
|
|
671
|
+
}
|
|
672
|
+
if (result.conflicts && result.conflicts.length > 0) {
|
|
673
|
+
lines.push(theme.warning(`Version conflict(s) across workspace (${result.conflicts.length}):`));
|
|
674
|
+
for (const conflict of result.conflicts) {
|
|
675
|
+
lines.push(` ${conflict.name}`);
|
|
676
|
+
for (const entry of conflict.versions) {
|
|
677
|
+
const manifests = entry.manifests.map((m) => {
|
|
678
|
+
const dir = m.replace(/\/package\.json$/, "");
|
|
679
|
+
const parts = dir.split("/");
|
|
680
|
+
return parts.slice(-2).join("/");
|
|
681
|
+
}).join(", ");
|
|
682
|
+
lines.push(` ${entry.version.padEnd(10)} ${theme.muted(manifests)}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
lines.push("");
|
|
686
|
+
}
|
|
687
|
+
if (result.unknownPackages && result.unknownPackages.length > 0) {
|
|
688
|
+
lines.push(theme.error("Unknown package(s) not found in workspace:"));
|
|
689
|
+
for (const name of result.unknownPackages) {
|
|
690
|
+
lines.push(` - ${name}`);
|
|
691
|
+
}
|
|
692
|
+
lines.push("");
|
|
693
|
+
}
|
|
694
|
+
if (!result.applied) {
|
|
695
|
+
if (options?.dryRun) {
|
|
696
|
+
lines.push(theme.muted("Dry run \u2014 no changes applied."));
|
|
697
|
+
} else if (result.updatesAvailable > 0) {
|
|
698
|
+
lines.push(theme.muted("Run 'outfitter upgrade --guide' for migration instructions."));
|
|
699
|
+
} else {
|
|
700
|
+
lines.push(theme.success("All packages are up to date."));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (options?.guide && result.guides && result.guides.length > 0) {
|
|
704
|
+
lines.push("", "=".repeat(60), "", "Migration Guide", "");
|
|
705
|
+
for (const guide of result.guides) {
|
|
706
|
+
const label = guide.breaking ? theme.error("BREAKING") : theme.success("non-breaking");
|
|
707
|
+
lines.push(`${theme.info(guide.packageName)} ${guide.fromVersion} -> ${guide.toVersion} [${label}]`);
|
|
708
|
+
if (guide.steps.length > 0) {
|
|
709
|
+
for (const step of guide.steps) {
|
|
710
|
+
lines.push(` ${step}`);
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
lines.push(` ${theme.muted("No migration steps available. Check release notes.")}`);
|
|
714
|
+
}
|
|
715
|
+
lines.push("");
|
|
716
|
+
}
|
|
717
|
+
} else if (options?.guide && result.updatesAvailable > 0 && !result.guides) {
|
|
718
|
+
const cwd = options.cwd ?? process.cwd();
|
|
719
|
+
const migrationsDir = findMigrationDocsDir(cwd);
|
|
720
|
+
if (migrationsDir) {
|
|
721
|
+
lines.push("", "=".repeat(60), "", "Migration Guide", "");
|
|
722
|
+
for (const pkg of result.packages) {
|
|
723
|
+
if (!(pkg.updateAvailable && pkg.latest))
|
|
724
|
+
continue;
|
|
725
|
+
const shortName = pkg.name.replace("@outfitter/", "");
|
|
726
|
+
const docs = readMigrationDocs(migrationsDir, shortName, pkg.current, pkg.latest);
|
|
727
|
+
for (const doc of docs) {
|
|
728
|
+
lines.push(doc, "", "---", "");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
lines.push("", theme.muted("Migration docs not found locally. See https://github.com/outfitter-dev/outfitter for migration guides."));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
await output(lines, { mode: "human" });
|
|
736
|
+
}
|
|
737
|
+
function writeUpgradeReport(cwd, result, meta) {
|
|
738
|
+
const reportsDir = join(cwd, ".outfitter", "reports");
|
|
739
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
740
|
+
const finishedAtIso = new Date().toISOString();
|
|
741
|
+
const errorContext = meta.error !== undefined && "context" in meta.error && meta.error.context !== undefined && typeof meta.error.context === "object" ? meta.error.context : undefined;
|
|
742
|
+
const report = {
|
|
743
|
+
$schema: "https://outfitter.dev/reports/upgrade/v1",
|
|
744
|
+
status: meta.status,
|
|
745
|
+
checkedAt: finishedAtIso,
|
|
746
|
+
startedAt: meta.startedAt.toISOString(),
|
|
747
|
+
finishedAt: finishedAtIso,
|
|
748
|
+
cwd,
|
|
749
|
+
workspaceRoot: meta.workspaceRoot,
|
|
750
|
+
flags: {
|
|
751
|
+
dryRun: meta.options.dryRun === true,
|
|
752
|
+
yes: meta.options.yes === true,
|
|
753
|
+
interactive: meta.options.interactive !== false,
|
|
754
|
+
all: meta.options.all === true,
|
|
755
|
+
noCodemods: meta.options.noCodemods === true,
|
|
756
|
+
outputMode: meta.options.outputMode ?? null
|
|
757
|
+
},
|
|
758
|
+
applied: result.applied,
|
|
759
|
+
summary: {
|
|
760
|
+
total: result.total,
|
|
761
|
+
available: result.updatesAvailable,
|
|
762
|
+
breaking: result.packages.filter((p) => p.breaking).length,
|
|
763
|
+
applied: result.appliedPackages.length
|
|
764
|
+
},
|
|
765
|
+
packages: result.packages,
|
|
766
|
+
excluded: {
|
|
767
|
+
breaking: result.skippedBreaking
|
|
768
|
+
},
|
|
769
|
+
...result.unknownPackages !== undefined && result.unknownPackages.length > 0 ? { unknownPackages: result.unknownPackages } : {},
|
|
770
|
+
...result.conflicts !== undefined && result.conflicts.length > 0 ? { conflicts: result.conflicts } : {},
|
|
771
|
+
...result.codemods !== undefined ? { codemods: result.codemods } : {},
|
|
772
|
+
...meta.error !== undefined ? {
|
|
773
|
+
error: {
|
|
774
|
+
message: meta.error.message,
|
|
775
|
+
category: meta.error.category,
|
|
776
|
+
...errorContext !== undefined ? { context: errorContext } : {}
|
|
777
|
+
}
|
|
778
|
+
} : {}
|
|
779
|
+
};
|
|
780
|
+
writeFileSync(join(reportsDir, "upgrade.json"), JSON.stringify(report, null, 2));
|
|
781
|
+
}
|
|
782
|
+
function writeUpgradeReportSafely(cwd, result, meta) {
|
|
783
|
+
try {
|
|
784
|
+
writeUpgradeReport(cwd, result, meta);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
787
|
+
process.stderr.write(`[outfitter upgrade] Failed to write report: ${reason}
|
|
788
|
+
`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// apps/outfitter/src/commands/upgrade-codemods.ts
|
|
793
|
+
var CODEMOD_PATHS = ["plugins/outfitter/shared/codemods"];
|
|
794
|
+
function findCodemodsDir(cwd, binaryDir) {
|
|
795
|
+
for (const relativePath of CODEMOD_PATHS) {
|
|
796
|
+
const dir = join2(cwd, relativePath);
|
|
797
|
+
if (existsSync2(dir))
|
|
798
|
+
return dir;
|
|
799
|
+
}
|
|
800
|
+
let current = resolve2(cwd);
|
|
801
|
+
const root = resolve2("/");
|
|
802
|
+
while (current !== root) {
|
|
803
|
+
const parent = resolve2(current, "..");
|
|
804
|
+
if (parent === current)
|
|
805
|
+
break;
|
|
806
|
+
current = parent;
|
|
807
|
+
for (const relativePath of CODEMOD_PATHS) {
|
|
808
|
+
const dir = join2(current, relativePath);
|
|
809
|
+
if (existsSync2(dir))
|
|
810
|
+
return dir;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const resolvedBinaryDir = binaryDir ?? resolve2(import.meta.dir, "../../../..");
|
|
814
|
+
for (const relativePath of CODEMOD_PATHS) {
|
|
815
|
+
const dir = join2(resolvedBinaryDir, relativePath);
|
|
816
|
+
if (existsSync2(dir))
|
|
817
|
+
return dir;
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
function discoverCodemods(migrationsDir, codemodsDir, shortName, fromVersion, toVersion) {
|
|
822
|
+
const resolvedCodemodsDir = resolve2(codemodsDir);
|
|
823
|
+
const docs = (() => {
|
|
824
|
+
try {
|
|
825
|
+
return readMigrationDocsWithMetadata(migrationsDir, shortName, fromVersion, toVersion);
|
|
826
|
+
} catch {
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
})();
|
|
830
|
+
const seen = new Set;
|
|
831
|
+
const codemods = [];
|
|
832
|
+
for (const doc of docs) {
|
|
833
|
+
if (!doc.frontmatter.changes)
|
|
834
|
+
continue;
|
|
835
|
+
for (const change of doc.frontmatter.changes) {
|
|
836
|
+
if (!change.codemod)
|
|
837
|
+
continue;
|
|
838
|
+
if (seen.has(change.codemod))
|
|
839
|
+
continue;
|
|
840
|
+
seen.add(change.codemod);
|
|
841
|
+
const absolutePath = resolveCodemodPath(resolvedCodemodsDir, change.codemod);
|
|
842
|
+
if (absolutePath === null)
|
|
843
|
+
continue;
|
|
844
|
+
if (!existsSync2(absolutePath))
|
|
845
|
+
continue;
|
|
846
|
+
codemods.push({
|
|
847
|
+
relativePath: change.codemod,
|
|
848
|
+
absolutePath
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return codemods;
|
|
853
|
+
}
|
|
854
|
+
function resolveCodemodPath(codemodsDir, relativePath) {
|
|
855
|
+
if (relativePath.trim().length === 0) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
if (isAbsolute(relativePath)) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
const resolvedPath = resolve2(codemodsDir, relativePath);
|
|
862
|
+
const relPath = relative(codemodsDir, resolvedPath);
|
|
863
|
+
if (relPath === "" || relPath.startsWith("..") || isAbsolute(relPath)) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
return resolvedPath;
|
|
867
|
+
}
|
|
868
|
+
async function runCodemod(codemodPath, targetDir, dryRun) {
|
|
869
|
+
let mod;
|
|
870
|
+
try {
|
|
871
|
+
mod = await import(codemodPath);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
return Result2.err(InternalError2.create("Failed to load codemod", {
|
|
874
|
+
codemodPath,
|
|
875
|
+
error: error instanceof Error ? error.message : String(error)
|
|
876
|
+
}));
|
|
877
|
+
}
|
|
878
|
+
if (typeof mod["transform"] !== "function") {
|
|
879
|
+
return Result2.err(InternalError2.create(`Codemod has no transform export: ${codemodPath}`, {
|
|
880
|
+
codemodPath
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
const transform = mod["transform"];
|
|
884
|
+
try {
|
|
885
|
+
const result = await transform({ targetDir, dryRun });
|
|
886
|
+
if (!isCodemodResult(result)) {
|
|
887
|
+
return Result2.err(InternalError2.create("Codemod returned invalid result shape", {
|
|
888
|
+
codemodPath
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
return Result2.ok(result);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
return Result2.err(InternalError2.create("Codemod execution failed", {
|
|
894
|
+
codemodPath,
|
|
895
|
+
error: error instanceof Error ? error.message : String(error)
|
|
896
|
+
}));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function isCodemodResult(value) {
|
|
900
|
+
if (typeof value !== "object" || value === null)
|
|
901
|
+
return false;
|
|
902
|
+
const candidate = value;
|
|
903
|
+
return isStringArray(candidate["changedFiles"]) && isStringArray(candidate["skippedFiles"]) && isStringArray(candidate["errors"]);
|
|
904
|
+
}
|
|
905
|
+
function isStringArray(value) {
|
|
906
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export { findCodemodsDir, discoverCodemods, runCodemod, findMigrationDocsDir, readMigrationDocs, readMigrationBreakingFlag, parseMigrationFrontmatter, readMigrationDocsWithMetadata, buildMigrationGuides, runUpgrade, printUpgradeResults };
|