@pgpmjs/core 7.4.2 → 7.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.
@@ -1,503 +0,0 @@
1
- import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
2
- import { generateChangeLineContent, generateTagLineContent } from '@pgpmjs/ast/files/plan/writer';
3
- import { minimatch } from 'minimatch';
4
- /**
5
- * Build a dependency graph from a parsed plan file
6
- */
7
- export function buildDependencyGraph(plan) {
8
- const graph = {
9
- nodes: new Map(),
10
- edges: new Map(),
11
- reverseEdges: new Map(),
12
- tags: new Map(),
13
- plan
14
- };
15
- // Add all changes as nodes
16
- for (const change of plan.changes) {
17
- graph.nodes.set(change.name, change);
18
- graph.edges.set(change.name, new Set(change.dependencies || []));
19
- // Build reverse edges for dependency analysis
20
- for (const dep of change.dependencies || []) {
21
- if (!graph.reverseEdges.has(dep)) {
22
- graph.reverseEdges.set(dep, new Set());
23
- }
24
- graph.reverseEdges.get(dep).add(change.name);
25
- }
26
- }
27
- // Index tags by change
28
- for (const tag of plan.tags || []) {
29
- if (!graph.tags.has(tag.change)) {
30
- graph.tags.set(tag.change, []);
31
- }
32
- graph.tags.get(tag.change).push(tag);
33
- }
34
- return graph;
35
- }
36
- /**
37
- * Validate that the dependency graph is a DAG (no cycles)
38
- */
39
- export function validateDAG(graph) {
40
- const visited = new Set();
41
- const visiting = new Set();
42
- function dfs(node, path) {
43
- if (visiting.has(node)) {
44
- throw new Error(`Cycle detected: ${[...path, node].join(' -> ')}`);
45
- }
46
- if (visited.has(node))
47
- return;
48
- visiting.add(node);
49
- const deps = graph.edges.get(node) || new Set();
50
- for (const dep of deps) {
51
- // Only follow internal dependencies
52
- if (graph.nodes.has(dep)) {
53
- dfs(dep, [...path, node]);
54
- }
55
- }
56
- visiting.delete(node);
57
- visited.add(node);
58
- }
59
- for (const node of graph.nodes.keys()) {
60
- dfs(node, []);
61
- }
62
- }
63
- /**
64
- * Extract package name from a change path using folder-based strategy
65
- */
66
- export function extractPackageFromPath(changeName, depth = 1, prefixToStrip = 'schemas') {
67
- const parts = changeName.split('/');
68
- // Handle special folders that should go to core
69
- if (parts[0] === 'extensions' || parts[0] === 'migrate') {
70
- return 'core';
71
- }
72
- // Strip prefix if present
73
- let startIdx = 0;
74
- if (parts[0] === prefixToStrip) {
75
- startIdx = 1;
76
- }
77
- // Get package name at specified depth
78
- if (parts.length > startIdx) {
79
- // For depth > 1, join multiple segments
80
- const endIdx = Math.min(startIdx + depth, parts.length - 1);
81
- if (endIdx > startIdx) {
82
- return parts.slice(startIdx, endIdx).join('/');
83
- }
84
- return parts[startIdx];
85
- }
86
- return 'core';
87
- }
88
- /**
89
- * Find the matching package for a change using pattern-based strategy.
90
- * Returns the first matching slice's package name, or undefined if no match.
91
- */
92
- export function findMatchingPattern(changeName, strategy) {
93
- for (const slice of strategy.slices) {
94
- for (const pattern of slice.patterns) {
95
- if (minimatch(changeName, pattern, { dot: true })) {
96
- return slice.packageName;
97
- }
98
- }
99
- }
100
- return undefined;
101
- }
102
- /**
103
- * Assign changes to packages based on grouping strategy
104
- */
105
- export function assignChangesToPackages(graph, strategy, defaultPackage = 'core') {
106
- const assignments = new Map();
107
- for (const [changeName] of graph.nodes) {
108
- let packageName;
109
- switch (strategy.type) {
110
- case 'folder': {
111
- const depth = strategy.depth ?? 1;
112
- const prefix = strategy.prefixToStrip ?? 'schemas';
113
- packageName = extractPackageFromPath(changeName, depth, prefix);
114
- break;
115
- }
116
- case 'pattern': {
117
- const matched = findMatchingPattern(changeName, strategy);
118
- packageName = matched || defaultPackage;
119
- break;
120
- }
121
- case 'explicit': {
122
- packageName = strategy.mapping[changeName] || defaultPackage;
123
- break;
124
- }
125
- default:
126
- packageName = defaultPackage;
127
- }
128
- // Fallback to default package
129
- if (!packageName) {
130
- packageName = defaultPackage;
131
- }
132
- if (!assignments.has(packageName)) {
133
- assignments.set(packageName, new Set());
134
- }
135
- assignments.get(packageName).add(changeName);
136
- }
137
- return assignments;
138
- }
139
- /**
140
- * Merge small packages into larger ones
141
- */
142
- export function mergeSmallPackages(assignments, minChanges, defaultPackage = 'core') {
143
- const result = new Map();
144
- // Ensure default package exists
145
- if (!result.has(defaultPackage)) {
146
- result.set(defaultPackage, new Set());
147
- }
148
- for (const [pkg, changes] of assignments) {
149
- if (changes.size < minChanges && pkg !== defaultPackage) {
150
- // Merge into default package
151
- const defaultChanges = result.get(defaultPackage);
152
- for (const change of changes) {
153
- defaultChanges.add(change);
154
- }
155
- }
156
- else {
157
- result.set(pkg, new Set(changes));
158
- }
159
- }
160
- // Remove empty default package if it exists
161
- if (result.get(defaultPackage)?.size === 0) {
162
- result.delete(defaultPackage);
163
- }
164
- return result;
165
- }
166
- /**
167
- * Build package-level dependency graph
168
- */
169
- export function buildPackageDependencies(graph, assignments) {
170
- const packageDeps = new Map();
171
- // Build reverse lookup: change -> package
172
- const changeToPackage = new Map();
173
- for (const [pkg, changes] of assignments) {
174
- packageDeps.set(pkg, new Set());
175
- for (const change of changes) {
176
- changeToPackage.set(change, pkg);
177
- }
178
- }
179
- // Check each change's dependencies
180
- for (const [changeName, deps] of graph.edges) {
181
- const myPackage = changeToPackage.get(changeName);
182
- if (!myPackage)
183
- continue;
184
- for (const dep of deps) {
185
- const depPackage = changeToPackage.get(dep);
186
- if (depPackage && depPackage !== myPackage) {
187
- // Cross-package dependency
188
- packageDeps.get(myPackage).add(depPackage);
189
- }
190
- }
191
- }
192
- return packageDeps;
193
- }
194
- /**
195
- * Detect cycles in package dependency graph
196
- */
197
- export function detectPackageCycle(deps) {
198
- const visited = new Set();
199
- const visiting = new Set();
200
- function dfs(pkg, path) {
201
- if (visiting.has(pkg)) {
202
- return [...path, pkg];
203
- }
204
- if (visited.has(pkg))
205
- return null;
206
- visiting.add(pkg);
207
- const pkgDeps = deps.get(pkg) || new Set();
208
- for (const dep of pkgDeps) {
209
- const cycle = dfs(dep, [...path, pkg]);
210
- if (cycle)
211
- return cycle;
212
- }
213
- visiting.delete(pkg);
214
- visited.add(pkg);
215
- return null;
216
- }
217
- for (const pkg of deps.keys()) {
218
- const cycle = dfs(pkg, []);
219
- if (cycle)
220
- return cycle;
221
- }
222
- return null;
223
- }
224
- /**
225
- * Compute deployment order for packages (topological sort)
226
- */
227
- export function computeDeployOrder(packageDeps) {
228
- const order = [];
229
- const visited = new Set();
230
- const allPackages = new Set();
231
- // Collect all packages
232
- for (const pkg of packageDeps.keys()) {
233
- allPackages.add(pkg);
234
- }
235
- for (const deps of packageDeps.values()) {
236
- for (const dep of deps) {
237
- allPackages.add(dep);
238
- }
239
- }
240
- function visit(pkg) {
241
- if (visited.has(pkg))
242
- return;
243
- visited.add(pkg);
244
- const deps = packageDeps.get(pkg) || new Set();
245
- for (const dep of deps) {
246
- visit(dep);
247
- }
248
- order.push(pkg);
249
- }
250
- // Visit all packages (sorted for determinism)
251
- for (const pkg of [...allPackages].sort()) {
252
- visit(pkg);
253
- }
254
- return order;
255
- }
256
- /**
257
- * Topological sort of changes within a package
258
- */
259
- export function topologicalSortWithinPackage(changes, graph) {
260
- const result = [];
261
- const visited = new Set();
262
- function visit(change) {
263
- if (visited.has(change))
264
- return;
265
- if (!changes.has(change))
266
- return;
267
- visited.add(change);
268
- const deps = graph.edges.get(change) || new Set();
269
- for (const dep of deps) {
270
- if (changes.has(dep)) {
271
- visit(dep);
272
- }
273
- }
274
- result.push(change);
275
- }
276
- // Visit in alphabetical order for determinism
277
- for (const change of [...changes].sort()) {
278
- visit(change);
279
- }
280
- return result;
281
- }
282
- /**
283
- * Generate plan file content for a package
284
- */
285
- export function generatePlanContent(pkgName, entries, tags = []) {
286
- let content = `%syntax-version=1.0.0\n`;
287
- content += `%project=${pkgName}\n`;
288
- content += `%uri=${pkgName}\n\n`;
289
- // Dependency brackets keep their original order (empty order map), matching
290
- // the source plans this content is sliced from.
291
- const originalOrder = new Map();
292
- for (const entry of entries) {
293
- content += generateChangeLineContent(entry, originalOrder) + '\n';
294
- // Add tags associated with this change
295
- const changeTags = tags.filter(t => t.change === entry.name);
296
- for (const tag of changeTags) {
297
- content += generateTagLineContent(tag) + '\n';
298
- }
299
- }
300
- return content;
301
- }
302
- /**
303
- * Generate control file content for a package
304
- */
305
- export function generateControlContent(pkgName, deps) {
306
- let content = `# ${pkgName} extension\n`;
307
- content += `comment = '${pkgName} module'\n`;
308
- content += `default_version = '0.0.1'\n`;
309
- content += `relocatable = false\n`;
310
- if (deps.size > 0) {
311
- content += `requires = '${[...deps].sort().join(', ')}'\n`;
312
- }
313
- return content;
314
- }
315
- /**
316
- * Generate a single package output
317
- */
318
- export function generateSinglePackage(pkgName, changes, graph, changeToPackage, pkgDeps, crossPackageDeps = 'change') {
319
- // Back-compat: earlier signature took useTagsForCrossPackageDeps: boolean
320
- const crossPackageDepMode = crossPackageDeps === true ? 'tag' : crossPackageDeps === false ? 'change' : crossPackageDeps;
321
- // Sort changes in topological order within package
322
- const sortedChanges = topologicalSortWithinPackage(changes, graph);
323
- // Build plan entries with updated dependencies
324
- const planEntries = [];
325
- for (const changeName of sortedChanges) {
326
- const originalChange = graph.nodes.get(changeName);
327
- const deps = graph.edges.get(changeName) || new Set();
328
- const newDeps = [];
329
- for (const dep of deps) {
330
- const depPkg = changeToPackage.get(dep);
331
- if (!depPkg) {
332
- // External dependency (from installed module) - keep as-is
333
- newDeps.push(dep);
334
- }
335
- else if (depPkg === pkgName) {
336
- // Internal dependency - keep as-is
337
- newDeps.push(dep);
338
- }
339
- else {
340
- // Cross-package dependency
341
- if (crossPackageDepMode === 'control-only') {
342
- // Dropped from the plan line; carried by the control file's
343
- // `requires` (extension install ordering deploys the whole
344
- // dependency package first)
345
- continue;
346
- }
347
- if (crossPackageDepMode === 'tag') {
348
- // Find latest tag in the dependency package
349
- const depPkgChanges = [...graph.nodes.keys()]
350
- .filter(c => changeToPackage.get(c) === depPkg);
351
- const lastChange = depPkgChanges[depPkgChanges.length - 1];
352
- const tags = graph.tags.get(lastChange);
353
- if (tags && tags.length > 0) {
354
- const latestTag = tags[tags.length - 1];
355
- newDeps.push(`${depPkg}:@${latestTag.name}`);
356
- }
357
- else {
358
- newDeps.push(`${depPkg}:${dep}`);
359
- }
360
- }
361
- else {
362
- newDeps.push(`${depPkg}:${dep}`);
363
- }
364
- }
365
- }
366
- planEntries.push({
367
- name: changeName,
368
- dependencies: newDeps,
369
- timestamp: originalChange.timestamp,
370
- planner: originalChange.planner,
371
- email: originalChange.email,
372
- comment: originalChange.comment
373
- });
374
- }
375
- // Collect tags for this package
376
- const packageTags = [];
377
- for (const changeName of sortedChanges) {
378
- const changeTags = graph.tags.get(changeName) || [];
379
- packageTags.push(...changeTags);
380
- }
381
- // Generate plan content
382
- const planContent = generatePlanContent(pkgName, planEntries, packageTags);
383
- // Generate control file content
384
- const controlContent = generateControlContent(pkgName, pkgDeps);
385
- return {
386
- name: pkgName,
387
- planContent,
388
- controlContent,
389
- changes: planEntries,
390
- packageDependencies: [...pkgDeps]
391
- };
392
- }
393
- /**
394
- * Main slicing function
395
- */
396
- export function slicePlan(config) {
397
- // Parse the source plan file
398
- const planResult = parsePlanFile(config.sourcePlan);
399
- if (!planResult.data) {
400
- const errorMessages = planResult.errors?.map(e => `Line ${e.line}: ${e.message}`).join('\n') || 'Unknown error';
401
- throw new Error(`Failed to parse plan file: ${errorMessages}`);
402
- }
403
- const plan = planResult.data;
404
- const warnings = [];
405
- // Build dependency graph
406
- const graph = buildDependencyGraph(plan);
407
- // Validate DAG
408
- try {
409
- validateDAG(graph);
410
- }
411
- catch (error) {
412
- throw new Error(`Invalid plan file: ${error.message}`);
413
- }
414
- // Assign changes to packages
415
- let assignments = assignChangesToPackages(graph, config.strategy, config.defaultPackage || 'core');
416
- // Merge small packages if configured
417
- if (config.minChangesPerPackage && config.minChangesPerPackage > 1) {
418
- assignments = mergeSmallPackages(assignments, config.minChangesPerPackage, config.defaultPackage || 'core');
419
- }
420
- // Build package dependencies
421
- const packageDeps = buildPackageDependencies(graph, assignments);
422
- // Check for package cycles
423
- const cycle = detectPackageCycle(packageDeps);
424
- if (cycle) {
425
- warnings.push({
426
- type: 'cycle_detected',
427
- message: `Package cycle detected: ${cycle.join(' -> ')}`,
428
- suggestedAction: 'Consider merging these packages or reorganizing dependencies'
429
- });
430
- // For now, we'll proceed but warn - in production we might want to auto-merge
431
- }
432
- // Build reverse lookup
433
- const changeToPackage = new Map();
434
- for (const [pkg, changes] of assignments) {
435
- for (const change of changes) {
436
- changeToPackage.set(change, pkg);
437
- }
438
- }
439
- // Compute deploy order
440
- const deployOrder = computeDeployOrder(packageDeps);
441
- // Generate packages
442
- const packages = [];
443
- for (const pkgName of deployOrder) {
444
- const changes = assignments.get(pkgName);
445
- if (!changes || changes.size === 0)
446
- continue;
447
- const pkgOutput = generateSinglePackage(pkgName, changes, graph, changeToPackage, packageDeps.get(pkgName) || new Set(), config.crossPackageDepMode ?? (config.useTagsForCrossPackageDeps ? 'tag' : 'change'));
448
- packages.push(pkgOutput);
449
- // Check for heavy cross-package dependencies
450
- let crossDeps = 0;
451
- let totalDeps = 0;
452
- for (const change of changes) {
453
- const deps = graph.edges.get(change) || new Set();
454
- for (const dep of deps) {
455
- totalDeps++;
456
- const depPkg = changeToPackage.get(dep);
457
- if (depPkg && depPkg !== pkgName) {
458
- crossDeps++;
459
- }
460
- }
461
- }
462
- const ratio = totalDeps > 0 ? crossDeps / totalDeps : 0;
463
- if (ratio > 0.5) {
464
- warnings.push({
465
- type: 'heavy_cross_deps',
466
- message: `Package "${pkgName}" has ${Math.round(ratio * 100)}% cross-package dependencies`,
467
- suggestedAction: 'Consider merging with dependent packages or reorganizing'
468
- });
469
- }
470
- }
471
- // Calculate stats
472
- let internalEdges = 0;
473
- let crossPackageEdges = 0;
474
- for (const [change, deps] of graph.edges) {
475
- const myPkg = changeToPackage.get(change);
476
- for (const dep of deps) {
477
- const depPkg = changeToPackage.get(dep);
478
- if (depPkg === myPkg) {
479
- internalEdges++;
480
- }
481
- else if (depPkg) {
482
- crossPackageEdges++;
483
- }
484
- }
485
- }
486
- const totalEdges = internalEdges + crossPackageEdges;
487
- return {
488
- packages,
489
- workspace: {
490
- packages: deployOrder,
491
- deployOrder,
492
- dependencies: Object.fromEntries([...packageDeps.entries()].map(([k, v]) => [k, [...v].sort()]))
493
- },
494
- warnings,
495
- stats: {
496
- totalChanges: graph.nodes.size,
497
- packagesCreated: packages.length,
498
- internalEdges,
499
- crossPackageEdges,
500
- crossPackageRatio: totalEdges > 0 ? crossPackageEdges / totalEdges : 0
501
- }
502
- };
503
- }
@@ -1 +0,0 @@
1
- export {};
package/slice/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './types';
2
- export * from './slice';
3
- export * from './output';
package/slice/index.js DELETED
@@ -1,19 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./types"), exports);
18
- __exportStar(require("./slice"), exports);
19
- __exportStar(require("./output"), exports);
package/slice/output.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import { SliceResult } from './types';
2
- /**
3
- * Options for writing sliced packages to disk
4
- */
5
- export interface WriteSliceOptions {
6
- /** Base output directory */
7
- outputDir: string;
8
- /** Whether to overwrite existing packages */
9
- overwrite?: boolean;
10
- /** Whether to copy SQL files from source */
11
- copySourceFiles?: boolean;
12
- /** Source directory containing deploy/revert/verify folders */
13
- sourceDir?: string;
14
- }
15
- /**
16
- * Write sliced packages to disk
17
- */
18
- export declare function writeSliceResult(result: SliceResult, options: WriteSliceOptions): void;
19
- /**
20
- * Generate a dry-run report of what would be created
21
- */
22
- export declare function generateDryRunReport(result: SliceResult): string;
package/slice/output.js DELETED
@@ -1,124 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeSliceResult = writeSliceResult;
7
- exports.generateDryRunReport = generateDryRunReport;
8
- const fs_1 = __importDefault(require("fs"));
9
- const path_1 = __importDefault(require("path"));
10
- /**
11
- * Write sliced packages to disk
12
- */
13
- function writeSliceResult(result, options) {
14
- const { outputDir, overwrite = false, copySourceFiles = false, sourceDir } = options;
15
- // Create output directory
16
- fs_1.default.mkdirSync(outputDir, { recursive: true });
17
- // Write each package
18
- for (const pkg of result.packages) {
19
- writePackage(pkg, outputDir, overwrite, copySourceFiles, sourceDir);
20
- }
21
- // Write workspace manifest
22
- const manifestPath = path_1.default.join(outputDir, 'pgpm-workspace.json');
23
- const manifestContent = JSON.stringify({
24
- packages: result.workspace.packages.map(p => `packages/${p}`),
25
- slicing: {
26
- deployOrder: result.workspace.deployOrder,
27
- dependencies: result.workspace.dependencies
28
- },
29
- stats: result.stats
30
- }, null, 2);
31
- fs_1.default.writeFileSync(manifestPath, manifestContent);
32
- }
33
- /**
34
- * Write a single package to disk
35
- */
36
- function writePackage(pkg, outputDir, overwrite, copySourceFiles, sourceDir) {
37
- const pkgDir = path_1.default.join(outputDir, 'packages', pkg.name);
38
- // Check if package already exists
39
- if (fs_1.default.existsSync(pkgDir) && !overwrite) {
40
- throw new Error(`Package directory already exists: ${pkgDir}. Use --overwrite to replace.`);
41
- }
42
- // Create package directory structure
43
- fs_1.default.mkdirSync(pkgDir, { recursive: true });
44
- fs_1.default.mkdirSync(path_1.default.join(pkgDir, 'deploy'), { recursive: true });
45
- fs_1.default.mkdirSync(path_1.default.join(pkgDir, 'revert'), { recursive: true });
46
- fs_1.default.mkdirSync(path_1.default.join(pkgDir, 'verify'), { recursive: true });
47
- // Write plan file
48
- fs_1.default.writeFileSync(path_1.default.join(pkgDir, 'pgpm.plan'), pkg.planContent);
49
- // Write control file
50
- fs_1.default.writeFileSync(path_1.default.join(pkgDir, `${pkg.name}.control`), pkg.controlContent);
51
- // Copy SQL files if requested
52
- if (copySourceFiles && sourceDir) {
53
- for (const change of pkg.changes) {
54
- copyChangeFiles(change.name, sourceDir, pkgDir);
55
- }
56
- }
57
- }
58
- /**
59
- * Copy deploy/revert/verify SQL files for a change
60
- */
61
- function copyChangeFiles(changeName, sourceDir, targetDir) {
62
- const types = ['deploy', 'revert', 'verify'];
63
- for (const type of types) {
64
- const sourceFile = path_1.default.join(sourceDir, type, `${changeName}.sql`);
65
- const targetFile = path_1.default.join(targetDir, type, `${changeName}.sql`);
66
- if (fs_1.default.existsSync(sourceFile)) {
67
- // Create target directory if needed
68
- const targetSubDir = path_1.default.dirname(targetFile);
69
- fs_1.default.mkdirSync(targetSubDir, { recursive: true });
70
- // Copy file
71
- fs_1.default.copyFileSync(sourceFile, targetFile);
72
- }
73
- }
74
- }
75
- /**
76
- * Generate a dry-run report of what would be created
77
- */
78
- function generateDryRunReport(result) {
79
- const lines = [];
80
- lines.push('=== PGPM Slice Dry Run Report ===\n');
81
- // Statistics
82
- lines.push('Statistics:');
83
- lines.push(` Total changes: ${result.stats.totalChanges}`);
84
- lines.push(` Packages to create: ${result.stats.packagesCreated}`);
85
- lines.push(` Internal edges: ${result.stats.internalEdges}`);
86
- lines.push(` Cross-package edges: ${result.stats.crossPackageEdges}`);
87
- lines.push(` Cross-package ratio: ${(result.stats.crossPackageRatio * 100).toFixed(1)}%`);
88
- lines.push('');
89
- // Warnings
90
- if (result.warnings.length > 0) {
91
- lines.push('Warnings:');
92
- for (const warning of result.warnings) {
93
- lines.push(` [${warning.type}] ${warning.message}`);
94
- if (warning.suggestedAction) {
95
- lines.push(` Suggestion: ${warning.suggestedAction}`);
96
- }
97
- }
98
- lines.push('');
99
- }
100
- // Deploy order
101
- lines.push('Deploy Order:');
102
- for (let i = 0; i < result.workspace.deployOrder.length; i++) {
103
- const pkg = result.workspace.deployOrder[i];
104
- const deps = result.workspace.dependencies[pkg] || [];
105
- const depStr = deps.length > 0 ? ` (depends on: ${deps.join(', ')})` : '';
106
- lines.push(` ${i + 1}. ${pkg}${depStr}`);
107
- }
108
- lines.push('');
109
- // Package details
110
- lines.push('Packages:');
111
- for (const pkg of result.packages) {
112
- lines.push(`\n ${pkg.name}/`);
113
- lines.push(` Changes: ${pkg.changes.length}`);
114
- lines.push(` Dependencies: ${pkg.packageDependencies.join(', ') || 'none'}`);
115
- lines.push(' Contents:');
116
- for (const change of pkg.changes.slice(0, 5)) {
117
- lines.push(` - ${change.name}`);
118
- }
119
- if (pkg.changes.length > 5) {
120
- lines.push(` ... and ${pkg.changes.length - 5} more`);
121
- }
122
- }
123
- return lines.join('\n');
124
- }