@pgpmjs/core 7.0.0 → 7.2.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.
Files changed (102) hide show
  1. package/bundle/apply.d.ts +93 -0
  2. package/bundle/apply.js +137 -0
  3. package/bundle/index.d.ts +12 -0
  4. package/bundle/index.js +31 -0
  5. package/core/class/pgpm.d.ts +2 -2
  6. package/core/class/pgpm.js +6 -6
  7. package/esm/bundle/apply.js +131 -0
  8. package/esm/bundle/index.js +15 -0
  9. package/esm/core/class/pgpm.js +6 -6
  10. package/esm/index.js +7 -3
  11. package/esm/migrate/client.js +1 -1
  12. package/esm/migrate/utils/hash.js +3 -6
  13. package/esm/modules/modules.js +1 -1
  14. package/esm/packaging/package.js +40 -24
  15. package/esm/rebundle/index.js +4 -0
  16. package/esm/rebundle/module.js +111 -0
  17. package/esm/rebundle/rebundle.js +170 -0
  18. package/esm/rebundle/workspace.js +170 -0
  19. package/esm/refactor/rename.js +5 -27
  20. package/esm/resolution/deps.js +2 -2
  21. package/esm/resolution/resolve.js +2 -2
  22. package/esm/slice/slice.js +1 -1
  23. package/esm/workspace/minimal.js +73 -0
  24. package/index.d.ts +6 -2
  25. package/index.js +8 -4
  26. package/migrate/client.js +1 -1
  27. package/migrate/utils/hash.d.ts +2 -4
  28. package/migrate/utils/hash.js +4 -8
  29. package/modules/modules.d.ts +1 -1
  30. package/modules/modules.js +1 -1
  31. package/package.json +4 -2
  32. package/packaging/package.d.ts +23 -1
  33. package/packaging/package.js +42 -25
  34. package/rebundle/index.d.ts +4 -0
  35. package/{files/sql-scripts → rebundle}/index.js +4 -2
  36. package/rebundle/module.d.ts +23 -0
  37. package/rebundle/module.js +116 -0
  38. package/rebundle/rebundle.d.ts +26 -0
  39. package/rebundle/rebundle.js +175 -0
  40. package/rebundle/types.d.ts +185 -0
  41. package/rebundle/workspace.d.ts +15 -0
  42. package/rebundle/workspace.js +173 -0
  43. package/refactor/rename.d.ts +2 -8
  44. package/refactor/rename.js +6 -29
  45. package/resolution/deps.d.ts +1 -1
  46. package/resolution/deps.js +2 -2
  47. package/resolution/resolve.js +2 -2
  48. package/slice/slice.d.ts +1 -1
  49. package/slice/slice.js +1 -1
  50. package/slice/types.d.ts +1 -1
  51. package/workspace/minimal.d.ts +64 -0
  52. package/workspace/minimal.js +82 -0
  53. package/esm/files/extension/index.js +0 -3
  54. package/esm/files/extension/reader.js +0 -79
  55. package/esm/files/extension/writer.js +0 -99
  56. package/esm/files/index.js +0 -6
  57. package/esm/files/plan/generator.js +0 -82
  58. package/esm/files/plan/index.js +0 -5
  59. package/esm/files/plan/parser.js +0 -297
  60. package/esm/files/plan/validators.js +0 -181
  61. package/esm/files/plan/writer.js +0 -142
  62. package/esm/files/sql/header.js +0 -259
  63. package/esm/files/sql/index.js +0 -2
  64. package/esm/files/sql/writer.js +0 -107
  65. package/esm/files/sql-scripts/index.js +0 -2
  66. package/esm/files/sql-scripts/reader.js +0 -19
  67. package/esm/files/types/index.js +0 -1
  68. package/esm/utils/author.js +0 -11
  69. package/files/extension/index.d.ts +0 -2
  70. package/files/extension/index.js +0 -19
  71. package/files/extension/reader.d.ts +0 -24
  72. package/files/extension/reader.js +0 -86
  73. package/files/extension/writer.d.ts +0 -48
  74. package/files/extension/writer.js +0 -107
  75. package/files/index.d.ts +0 -5
  76. package/files/index.js +0 -22
  77. package/files/plan/generator.d.ts +0 -23
  78. package/files/plan/generator.js +0 -90
  79. package/files/plan/index.d.ts +0 -4
  80. package/files/plan/index.js +0 -21
  81. package/files/plan/parser.d.ts +0 -28
  82. package/files/plan/parser.js +0 -304
  83. package/files/plan/validators.d.ts +0 -52
  84. package/files/plan/validators.js +0 -187
  85. package/files/plan/writer.d.ts +0 -27
  86. package/files/plan/writer.js +0 -152
  87. package/files/sql/header.d.ts +0 -114
  88. package/files/sql/header.js +0 -266
  89. package/files/sql/index.d.ts +0 -2
  90. package/files/sql/index.js +0 -18
  91. package/files/sql/writer.d.ts +0 -12
  92. package/files/sql/writer.js +0 -114
  93. package/files/sql-scripts/index.d.ts +0 -1
  94. package/files/sql-scripts/reader.d.ts +0 -8
  95. package/files/sql-scripts/reader.js +0 -23
  96. package/files/types/index.d.ts +0 -46
  97. package/files/types/index.js +0 -17
  98. package/files/types/package.d.ts +0 -20
  99. package/utils/author.d.ts +0 -5
  100. package/utils/author.js +0 -14
  101. /package/esm/{files/types/package.js → rebundle/types.js} +0 -0
  102. /package/{files/types/package.js → rebundle/types.js} +0 -0
@@ -1,79 +0,0 @@
1
- import { readFileSync } from 'fs';
2
- import { basename, dirname, relative } from 'path';
3
- import { parsePlanFileSimple } from '../plan';
4
- /**
5
- * Parse a .control file and extract its metadata.
6
- * https://www.postgresql.org/docs/current/extend-extensions.html
7
- */
8
- export function parseControlFile(filePath, basePath) {
9
- const contents = readFileSync(filePath, 'utf-8');
10
- const key = basename(filePath).split('.control')[0];
11
- const requires = contents
12
- .split('\n')
13
- .find((line) => /^requires/.test(line))
14
- ?.split('=')[1]
15
- .split(',')
16
- .map((req) => req.replace(/[\'\s]*/g, '').trim()) || [];
17
- const version = contents
18
- .split('\n')
19
- .find((line) => /^default_version/.test(line))
20
- ?.split('=')[1]
21
- .replace(/[\']*/g, '')
22
- .trim() || '';
23
- return {
24
- path: dirname(relative(basePath, filePath)),
25
- requires,
26
- version,
27
- };
28
- }
29
- /**
30
- * Parse the pgpm.plan file to get the extension name.
31
- */
32
- export const getExtensionName = (packageDir) => {
33
- const planPath = `${packageDir}/pgpm.plan`;
34
- const plan = parsePlanFileSimple(planPath);
35
- if (!plan.package) {
36
- throw new Error('No package name found in pgpm.plan!');
37
- }
38
- return plan.package;
39
- };
40
- /**
41
- * Get detailed information about an extension in the specified directory.
42
- */
43
- export const getExtensionInfo = (packageDir) => {
44
- const pkgPath = `${packageDir}/package.json`;
45
- const pkg = require(pkgPath);
46
- const extname = getExtensionName(packageDir);
47
- const version = pkg.version;
48
- const Makefile = `${packageDir}/Makefile`;
49
- const controlFile = `${packageDir}/${extname}.control`;
50
- const sqlFile = `sql/${extname}--${version}.sql`;
51
- return { extname, packageDir, version, Makefile, controlFile, sqlFile };
52
- };
53
- /**
54
- * Get a list of extensions required by an extension from its control file.
55
- * Returns an empty array if the file doesn't exist or has no requires line.
56
- */
57
- export const getInstalledExtensions = (controlFilePath) => {
58
- try {
59
- const contents = readFileSync(controlFilePath, 'utf-8');
60
- const match = contents.match(/^\s*requires\s*=\s*'([^']*)'/m);
61
- if (!match) {
62
- return [];
63
- }
64
- const requiresValue = match[1];
65
- if (!requiresValue || requiresValue.trim() === '') {
66
- return [];
67
- }
68
- return requiresValue
69
- .split(',')
70
- .map((ext) => ext.trim())
71
- .filter((ext) => ext.length > 0);
72
- }
73
- catch (e) {
74
- if (e.code === 'ENOENT') {
75
- return [];
76
- }
77
- throw new Error(`Error reading control file at ${controlFilePath}: ${e.message}`);
78
- }
79
- };
@@ -1,99 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'fs';
2
- import { getExtensionInfo } from './reader';
3
- /**
4
- * Write the Makefile for the extension.
5
- */
6
- export const writeExtensionMakefile = (outputPath, extname, version) => {
7
- const content = `EXTENSION = ${extname}
8
- DATA = sql/${extname}--${version}.sql
9
-
10
- PG_CONFIG = pg_config
11
- PGXS := $(shell $(PG_CONFIG) --pgxs)
12
- include $(PGXS)
13
- `;
14
- writeFileSync(outputPath, content);
15
- };
16
- /**
17
- * Generate content for a .control file
18
- * https://www.postgresql.org/docs/current/extend-extensions.html
19
- */
20
- export function generateControlFileContent(options) {
21
- const { name, version, comment = `${name} extension`, requires = [], default_version = version, relocatable = false, superuser = false, schema, module_pathname } = options;
22
- let content = `# ${name} extension
23
- comment = '${comment}'
24
- default_version = '${default_version}'
25
- `;
26
- if (module_pathname) {
27
- content += `module_pathname = '${module_pathname}'\n`;
28
- }
29
- else {
30
- content += `module_pathname = '$libdir/${name}'\n`;
31
- }
32
- if (requires.length > 0) {
33
- content += `requires = '${requires.join(',')}'\n`;
34
- }
35
- content += `relocatable = ${relocatable}
36
- superuser = ${superuser}\n`;
37
- if (schema) {
38
- content += `schema = ${schema}\n`;
39
- }
40
- return content;
41
- }
42
- /**
43
- * Update (or insert/remove) the `requires =` line of an existing .control file
44
- * in place, preserving every other field and the file's formatting. Used when
45
- * changing a module's dependencies after init (e.g. via `pgpm extension`) so
46
- * hand-tuned fields (comment, schema, relocatable, module_pathname) survive.
47
- */
48
- export function upsertRequiresLine(content, requires) {
49
- const lines = content.split('\n');
50
- const idx = lines.findIndex((line) => /^\s*requires\s*=/.test(line));
51
- if (requires.length === 0) {
52
- if (idx !== -1)
53
- lines.splice(idx, 1);
54
- return lines.join('\n');
55
- }
56
- const requiresLine = `requires = '${requires.join(',')}'`;
57
- if (idx !== -1) {
58
- lines[idx] = requiresLine;
59
- return lines.join('\n');
60
- }
61
- // Insert to match generated ordering (before relocatable/superuser/schema).
62
- const insertAt = lines.findIndex((line) => /^\s*(relocatable|superuser|schema)\s*=/.test(line));
63
- if (insertAt !== -1) {
64
- lines.splice(insertAt, 0, requiresLine);
65
- }
66
- else {
67
- lines.push(requiresLine);
68
- }
69
- return lines.join('\n');
70
- }
71
- /**
72
- * Write the control file for the extension.
73
- * If the file already exists, only its `requires` line is updated so that any
74
- * custom fields are preserved; otherwise a fresh file is generated.
75
- */
76
- export const writeExtensionControlFile = (outputPath, extname, extensions, version) => {
77
- if (existsSync(outputPath)) {
78
- const existing = readFileSync(outputPath, 'utf-8');
79
- writeFileSync(outputPath, upsertRequiresLine(existing, extensions));
80
- return;
81
- }
82
- const content = generateControlFileContent({
83
- name: extname,
84
- version,
85
- requires: extensions
86
- });
87
- writeFileSync(outputPath, content);
88
- };
89
- /**
90
- * Write control and Makefile for the extension with given data.
91
- * If extInfo is not provided, it will be generated from packageDir.
92
- */
93
- export const writeExtensions = (packageDir, extensions, extInfo) => {
94
- // If extInfo is not provided, get it from packageDir
95
- const info = extInfo || getExtensionInfo(packageDir);
96
- const { controlFile, Makefile, extname, version } = info;
97
- writeExtensionControlFile(controlFile, extname, extensions, version);
98
- writeExtensionMakefile(Makefile, extname, version);
99
- };
@@ -1,6 +0,0 @@
1
- // Re-export all functionality from package-files
2
- export * from './extension';
3
- export * from './plan';
4
- export * from './sql';
5
- export * from './sql-scripts';
6
- export * from './types';
@@ -1,82 +0,0 @@
1
- import fs from 'fs';
2
- /**
3
- * Fixed epoch timestamp used for deterministic plan generation.
4
- * Can be overridden with the PGPM_FROZEN_TIME environment variable.
5
- */
6
- const DEFAULT_FROZEN_TIME = '2017-08-11T08:11:51Z';
7
- /**
8
- * Get a UTC timestamp string from a Date object.
9
- */
10
- function getUTCTimestamp(d) {
11
- return (d.getUTCFullYear() +
12
- '-' + String(d.getUTCMonth() + 1).padStart(2, '0') +
13
- '-' + String(d.getUTCDate()).padStart(2, '0') +
14
- 'T' + String(d.getUTCHours()).padStart(2, '0') +
15
- ':' + String(d.getUTCMinutes()).padStart(2, '0') +
16
- ':' + String(d.getUTCSeconds()).padStart(2, '0') +
17
- 'Z');
18
- }
19
- /**
20
- * Get a timestamp for the plan file.
21
- * Uses the PGPM_FROZEN_TIME env var if present, otherwise the fixed epoch.
22
- */
23
- export function getNow() {
24
- const frozen = process.env.PGPM_FROZEN_TIME;
25
- if (frozen) {
26
- return getUTCTimestamp(new Date(frozen));
27
- }
28
- return DEFAULT_FROZEN_TIME;
29
- }
30
- /**
31
- * Sort dependencies by their order in the supplied plan, so prerequisites
32
- * (parent migrations) appear before their dependents in the bracket.
33
- * Internal dependencies are sorted by their plan index, and external
34
- * dependencies (not in the plan) preserve their original order.
35
- */
36
- const sortDependencies = (deps, order) => {
37
- const originalOrder = new Map(deps.map((dep, index) => [dep, index]));
38
- return [...deps].sort((a, b) => {
39
- const aIndex = order.get(a);
40
- const bIndex = order.get(b);
41
- const aOriginal = originalOrder.get(a);
42
- const bOriginal = originalOrder.get(b);
43
- if (aIndex !== undefined && bIndex !== undefined)
44
- return aIndex - bIndex || aOriginal - bOriginal;
45
- if (aIndex !== undefined)
46
- return -1;
47
- if (bIndex !== undefined)
48
- return 1;
49
- return aOriginal - bOriginal;
50
- });
51
- };
52
- /**
53
- * Generate a plan file content from structured data
54
- */
55
- export function generatePlan(options) {
56
- const { moduleName, uri, entries } = options;
57
- const now = getNow();
58
- const planfile = [
59
- `%syntax-version=1.0.0`,
60
- `%project=${moduleName}`,
61
- `%uri=${uri || moduleName}`
62
- ];
63
- // Build an order map from the entry list so brackets can be sorted by plan order
64
- const entryOrder = new Map(entries.map((entry, index) => [entry.change, index]));
65
- // Generate the plan entries
66
- entries.forEach(entry => {
67
- const deps = sortDependencies(entry.dependencies || [], entryOrder);
68
- if (deps.length > 0) {
69
- planfile.push(`${entry.change} [${deps.join(' ')}] ${now} constructive <constructive@5b0c196eeb62>${entry.comment ? ` # ${entry.comment}` : ''}`);
70
- }
71
- else {
72
- planfile.push(`${entry.change} ${now} constructive <constructive@5b0c196eeb62>${entry.comment ? ` # ${entry.comment}` : ''}`);
73
- }
74
- });
75
- return planfile.join('\n');
76
- }
77
- /**
78
- * Write a generated plan file to disk
79
- */
80
- export function writePlan(planPath, plan) {
81
- fs.writeFileSync(planPath, plan);
82
- }
@@ -1,5 +0,0 @@
1
- // Re-export all plan functionality
2
- export * from './generator';
3
- export * from './parser';
4
- export * from './validators';
5
- export * from './writer';
@@ -1,297 +0,0 @@
1
- import { readFileSync } from 'fs';
2
- import { isValidChangeName, isValidDependency, isValidTagName, parseReference } from './validators';
3
- import { errors } from '@pgpmjs/types';
4
- /**
5
- * Parse a Sqitch plan file with full validation
6
- * Supports both changes and tags
7
- */
8
- export function parsePlanFile(planPath) {
9
- const content = readFileSync(planPath, 'utf-8');
10
- const lines = content.split('\n');
11
- let project = '';
12
- let uri = '';
13
- const changes = [];
14
- const tags = [];
15
- const errors = [];
16
- for (let i = 0; i < lines.length; i++) {
17
- const line = lines[i];
18
- const lineNum = i + 1;
19
- const trimmed = line.trim();
20
- // Skip empty lines
21
- if (!trimmed)
22
- continue;
23
- // Skip comments
24
- if (trimmed.startsWith('#'))
25
- continue;
26
- // Parse package metadata
27
- if (trimmed.startsWith('%project=')) {
28
- project = trimmed.substring('%project='.length);
29
- continue;
30
- }
31
- if (trimmed.startsWith('%uri=')) {
32
- uri = trimmed.substring('%uri='.length);
33
- continue;
34
- }
35
- // Skip other metadata lines
36
- if (trimmed.startsWith('%')) {
37
- continue;
38
- }
39
- // Parse tag lines
40
- if (trimmed.startsWith('@')) {
41
- const lastChangeName = changes.length > 0 ? changes[changes.length - 1].name : null;
42
- const tag = parseTagLine(trimmed, lastChangeName);
43
- if (tag) {
44
- if (isValidTagName(tag.name)) {
45
- tags.push(tag);
46
- }
47
- else {
48
- errors.push({
49
- line: lineNum,
50
- message: `Invalid tag name: ${tag.name}`
51
- });
52
- }
53
- }
54
- else {
55
- errors.push({
56
- line: lineNum,
57
- message: 'Invalid tag line format'
58
- });
59
- }
60
- continue;
61
- }
62
- // Parse change lines
63
- const change = parseChangeLine(trimmed);
64
- if (change) {
65
- // Validate change name
66
- if (!isValidChangeName(change.name)) {
67
- errors.push({
68
- line: lineNum,
69
- message: `Invalid change name: ${change.name}`
70
- });
71
- continue;
72
- }
73
- // Validate dependencies
74
- for (const dep of change.dependencies) {
75
- if (!isValidDependency(dep)) {
76
- errors.push({
77
- line: lineNum,
78
- message: `Invalid dependency reference: ${dep}`
79
- });
80
- }
81
- }
82
- changes.push(change);
83
- }
84
- else if (trimmed) {
85
- // Non-empty line that couldn't be parsed
86
- errors.push({
87
- line: lineNum,
88
- message: 'Invalid line format'
89
- });
90
- }
91
- }
92
- if (errors.length > 0) {
93
- return { errors };
94
- }
95
- return {
96
- data: { package: project, uri, changes, tags },
97
- errors: []
98
- };
99
- }
100
- /**
101
- * Parse a single change line from a plan file
102
- * Format: change_name [deps] timestamp planner <email> # comment
103
- */
104
- function parseChangeLine(line) {
105
- // More flexible regex that handles various formats, including planner names with spaces
106
- // Format: change_name [deps] timestamp planner <email> # comment
107
- // The timestamp is required if planner/email/comment are present
108
- const regex = /^(\S+)(?:\s+\[([^\]]*)\])?(?:\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)(?:\s+([^<]+?))?(?:\s+<([^>]+)>)?(?:\s+#\s+(.*))?)?$/;
109
- const match = line.match(regex);
110
- if (!match) {
111
- return null;
112
- }
113
- const [, name, depsStr, timestamp, planner, email, comment] = match;
114
- // Parse dependencies
115
- const dependencies = depsStr
116
- ? depsStr.split(/\s+/).filter(dep => dep.length > 0)
117
- : [];
118
- return {
119
- name,
120
- dependencies,
121
- timestamp,
122
- planner,
123
- email,
124
- comment
125
- };
126
- }
127
- /**
128
- * Parse a tag line from a plan file
129
- * Format: @tag_name change_name timestamp planner <email> # comment
130
- */
131
- function parseTagLine(line, lastChangeName) {
132
- // Tag lines start with @
133
- if (!line.startsWith('@')) {
134
- return null;
135
- }
136
- // Remove the @ and parse
137
- const tagContent = line.substring(1);
138
- // Two possible formats:
139
- // 1. tagname timestamp planner <email> # comment (tag for last change)
140
- // 2. tagname changename timestamp planner <email> # comment (tag for specific change)
141
- // First try to match with change name (format 2)
142
- const regexWithChange = /^(\S+)\s+(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/;
143
- let match = tagContent.match(regexWithChange);
144
- if (match) {
145
- const [, name, secondToken, timestamp, planner, email, comment] = match;
146
- // Check if the second token is a timestamp (format 1) or change name (format 2)
147
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(secondToken)) {
148
- // Format 1: no change name specified
149
- return {
150
- name,
151
- change: lastChangeName || '', // Tag is associated with the last change
152
- timestamp: secondToken,
153
- planner: timestamp, // What we thought was timestamp is actually planner
154
- email: planner, // Shift everything
155
- comment: email || comment
156
- };
157
- }
158
- else {
159
- // Format 2: explicit change name
160
- return {
161
- name,
162
- change: secondToken,
163
- timestamp,
164
- planner,
165
- email,
166
- comment
167
- };
168
- }
169
- }
170
- // Try simple format without change name
171
- const regexSimple = /^(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/;
172
- match = tagContent.match(regexSimple);
173
- if (!match) {
174
- return null;
175
- }
176
- const [, name, timestamp, planner, email, comment] = match;
177
- return {
178
- name,
179
- change: lastChangeName || '', // Tag is associated with the last change
180
- timestamp,
181
- planner,
182
- email,
183
- comment
184
- };
185
- }
186
- /**
187
- * Resolve a reference within a plan file context
188
- * Handles symbolic references (HEAD, ROOT) and relative references
189
- */
190
- export function resolveReference(ref, plan, currentPackage) {
191
- const parsed = parseReference(ref);
192
- if (!parsed) {
193
- return { error: `Invalid reference: ${ref}` };
194
- }
195
- // Handle package qualifier
196
- const planPackage = currentPackage || plan.package;
197
- if (parsed.package && parsed.package !== planPackage) {
198
- // Cross-package reference - return as-is
199
- return { change: ref };
200
- }
201
- // Handle SHA1 references
202
- if (parsed.sha1) {
203
- // Would need to look up in database
204
- return { change: ref };
205
- }
206
- // Handle symbolic references
207
- if (parsed.symbolic === 'HEAD' && plan.changes.length > 0) {
208
- return { change: plan.changes[plan.changes.length - 1].name };
209
- }
210
- if (parsed.symbolic === 'ROOT' && plan.changes.length > 0) {
211
- return { change: plan.changes[0].name };
212
- }
213
- // Handle relative references
214
- if (parsed.relative) {
215
- const baseResolved = resolveReference(parsed.relative.base, plan, currentPackage);
216
- if (baseResolved.error) {
217
- return baseResolved;
218
- }
219
- // Get the change name from the resolved reference
220
- const baseChange = baseResolved.change;
221
- if (!baseChange) {
222
- return { error: `Cannot resolve base reference: ${parsed.relative.base}` };
223
- }
224
- const changeIndex = plan.changes.findIndex(c => c.name === baseChange);
225
- if (changeIndex === -1) {
226
- return { error: `Change not found: ${baseChange}` };
227
- }
228
- let targetIndex;
229
- if (parsed.relative.direction === '^') {
230
- // Go backwards
231
- targetIndex = changeIndex - parsed.relative.count;
232
- }
233
- else {
234
- // Go forwards
235
- targetIndex = changeIndex + parsed.relative.count;
236
- }
237
- if (targetIndex < 0 || targetIndex >= plan.changes.length) {
238
- return { error: `Relative reference out of bounds: ${ref}` };
239
- }
240
- return { change: plan.changes[targetIndex].name };
241
- }
242
- // Handle tag references
243
- if (parsed.tag) {
244
- const tag = plan.tags.find(t => t.name === parsed.tag);
245
- if (!tag) {
246
- return { error: `Tag not found: ${parsed.tag}` };
247
- }
248
- return { tag: parsed.tag, change: tag.change };
249
- }
250
- // Handle change@tag format
251
- if (parsed.change && parsed.tag) {
252
- return { change: parsed.change, tag: parsed.tag };
253
- }
254
- // Plain change reference
255
- if (parsed.change) {
256
- // Validate that the change exists in the plan
257
- const changeExists = plan.changes.some(c => c.name === parsed.change);
258
- if (!changeExists) {
259
- return { error: `Change not found: ${parsed.change}` };
260
- }
261
- return { change: parsed.change };
262
- }
263
- return { error: `Cannot resolve reference: ${ref}` };
264
- }
265
- /**
266
- * Simple plan file parser without validation.
267
- * Returns a PlanFile (without tags) for callers that don't need tag data.
268
- */
269
- export function parsePlanFileSimple(planPath) {
270
- const result = parsePlanFile(planPath);
271
- if (result.data) {
272
- // Return without tags for simple format
273
- const { tags, ...planWithoutTags } = result.data;
274
- return planWithoutTags;
275
- }
276
- // If there are errors, throw with details
277
- if (result.errors && result.errors.length > 0) {
278
- const errorMessages = result.errors.map(e => `Line ${e.line}: ${e.message}`).join('\n');
279
- throw errors.PLAN_PARSE_ERROR({ planPath, errors: errorMessages });
280
- }
281
- // Return empty plan if no data and no errors
282
- return { package: '', uri: '', changes: [] };
283
- }
284
- /**
285
- * Get all change names from a plan file
286
- */
287
- export function getChanges(planPath) {
288
- const plan = parsePlanFileSimple(planPath);
289
- return plan.changes.map(change => change.name);
290
- }
291
- /**
292
- * Get the latest (last) change from a plan file
293
- */
294
- export function getLatestChange(planPath) {
295
- const changes = getChanges(planPath);
296
- return changes[changes.length - 1] || '';
297
- }