@pgpmjs/core 3.0.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 (140) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +99 -0
  3. package/core/boilerplate-scanner.d.ts +41 -0
  4. package/core/boilerplate-scanner.js +106 -0
  5. package/core/boilerplate-types.d.ts +52 -0
  6. package/core/boilerplate-types.js +6 -0
  7. package/core/class/pgpm.d.ts +150 -0
  8. package/core/class/pgpm.js +1470 -0
  9. package/core/template-scaffold.d.ts +29 -0
  10. package/core/template-scaffold.js +168 -0
  11. package/esm/core/boilerplate-scanner.js +96 -0
  12. package/esm/core/boilerplate-types.js +5 -0
  13. package/esm/core/class/pgpm.js +1430 -0
  14. package/esm/core/template-scaffold.js +161 -0
  15. package/esm/export/export-meta.js +240 -0
  16. package/esm/export/export-migrations.js +180 -0
  17. package/esm/extensions/extensions.js +31 -0
  18. package/esm/files/extension/index.js +3 -0
  19. package/esm/files/extension/reader.js +79 -0
  20. package/esm/files/extension/writer.js +63 -0
  21. package/esm/files/index.js +6 -0
  22. package/esm/files/plan/generator.js +49 -0
  23. package/esm/files/plan/index.js +5 -0
  24. package/esm/files/plan/parser.js +296 -0
  25. package/esm/files/plan/validators.js +181 -0
  26. package/esm/files/plan/writer.js +114 -0
  27. package/esm/files/sql/index.js +1 -0
  28. package/esm/files/sql/writer.js +107 -0
  29. package/esm/files/sql-scripts/index.js +2 -0
  30. package/esm/files/sql-scripts/reader.js +19 -0
  31. package/esm/files/types/index.js +1 -0
  32. package/esm/files/types/package.js +1 -0
  33. package/esm/index.js +21 -0
  34. package/esm/init/client.js +144 -0
  35. package/esm/init/sql/bootstrap-roles.sql +55 -0
  36. package/esm/init/sql/bootstrap-test-roles.sql +72 -0
  37. package/esm/migrate/clean.js +23 -0
  38. package/esm/migrate/client.js +551 -0
  39. package/esm/migrate/index.js +5 -0
  40. package/esm/migrate/sql/procedures.sql +258 -0
  41. package/esm/migrate/sql/schema.sql +37 -0
  42. package/esm/migrate/types.js +1 -0
  43. package/esm/migrate/utils/event-logger.js +28 -0
  44. package/esm/migrate/utils/hash.js +27 -0
  45. package/esm/migrate/utils/transaction.js +125 -0
  46. package/esm/modules/modules.js +49 -0
  47. package/esm/packaging/package.js +96 -0
  48. package/esm/packaging/transform.js +70 -0
  49. package/esm/projects/deploy.js +123 -0
  50. package/esm/projects/revert.js +75 -0
  51. package/esm/projects/verify.js +61 -0
  52. package/esm/resolution/deps.js +526 -0
  53. package/esm/resolution/resolve.js +101 -0
  54. package/esm/utils/debug.js +147 -0
  55. package/esm/utils/target-utils.js +37 -0
  56. package/esm/workspace/paths.js +43 -0
  57. package/esm/workspace/utils.js +31 -0
  58. package/export/export-meta.d.ts +8 -0
  59. package/export/export-meta.js +244 -0
  60. package/export/export-migrations.d.ts +17 -0
  61. package/export/export-migrations.js +187 -0
  62. package/extensions/extensions.d.ts +5 -0
  63. package/extensions/extensions.js +35 -0
  64. package/files/extension/index.d.ts +2 -0
  65. package/files/extension/index.js +19 -0
  66. package/files/extension/reader.d.ts +24 -0
  67. package/files/extension/reader.js +86 -0
  68. package/files/extension/writer.d.ts +39 -0
  69. package/files/extension/writer.js +70 -0
  70. package/files/index.d.ts +5 -0
  71. package/files/index.js +22 -0
  72. package/files/plan/generator.d.ts +22 -0
  73. package/files/plan/generator.js +57 -0
  74. package/files/plan/index.d.ts +4 -0
  75. package/files/plan/index.js +21 -0
  76. package/files/plan/parser.d.ts +27 -0
  77. package/files/plan/parser.js +303 -0
  78. package/files/plan/validators.d.ts +52 -0
  79. package/files/plan/validators.js +187 -0
  80. package/files/plan/writer.d.ts +27 -0
  81. package/files/plan/writer.js +124 -0
  82. package/files/sql/index.d.ts +1 -0
  83. package/files/sql/index.js +17 -0
  84. package/files/sql/writer.d.ts +12 -0
  85. package/files/sql/writer.js +114 -0
  86. package/files/sql-scripts/index.d.ts +1 -0
  87. package/files/sql-scripts/index.js +18 -0
  88. package/files/sql-scripts/reader.d.ts +8 -0
  89. package/files/sql-scripts/reader.js +23 -0
  90. package/files/types/index.d.ts +46 -0
  91. package/files/types/index.js +17 -0
  92. package/files/types/package.d.ts +20 -0
  93. package/files/types/package.js +2 -0
  94. package/index.d.ts +21 -0
  95. package/index.js +45 -0
  96. package/init/client.d.ts +26 -0
  97. package/init/client.js +148 -0
  98. package/init/sql/bootstrap-roles.sql +55 -0
  99. package/init/sql/bootstrap-test-roles.sql +72 -0
  100. package/migrate/clean.d.ts +1 -0
  101. package/migrate/clean.js +27 -0
  102. package/migrate/client.d.ts +80 -0
  103. package/migrate/client.js +555 -0
  104. package/migrate/index.d.ts +5 -0
  105. package/migrate/index.js +21 -0
  106. package/migrate/sql/procedures.sql +258 -0
  107. package/migrate/sql/schema.sql +37 -0
  108. package/migrate/types.d.ts +67 -0
  109. package/migrate/types.js +2 -0
  110. package/migrate/utils/event-logger.d.ts +13 -0
  111. package/migrate/utils/event-logger.js +32 -0
  112. package/migrate/utils/hash.d.ts +12 -0
  113. package/migrate/utils/hash.js +32 -0
  114. package/migrate/utils/transaction.d.ts +27 -0
  115. package/migrate/utils/transaction.js +129 -0
  116. package/modules/modules.d.ts +31 -0
  117. package/modules/modules.js +56 -0
  118. package/package.json +70 -0
  119. package/packaging/package.d.ts +19 -0
  120. package/packaging/package.js +102 -0
  121. package/packaging/transform.d.ts +22 -0
  122. package/packaging/transform.js +75 -0
  123. package/projects/deploy.d.ts +8 -0
  124. package/projects/deploy.js +160 -0
  125. package/projects/revert.d.ts +15 -0
  126. package/projects/revert.js +112 -0
  127. package/projects/verify.d.ts +8 -0
  128. package/projects/verify.js +98 -0
  129. package/resolution/deps.d.ts +57 -0
  130. package/resolution/deps.js +531 -0
  131. package/resolution/resolve.d.ts +37 -0
  132. package/resolution/resolve.js +107 -0
  133. package/utils/debug.d.ts +21 -0
  134. package/utils/debug.js +153 -0
  135. package/utils/target-utils.d.ts +5 -0
  136. package/utils/target-utils.js +40 -0
  137. package/workspace/paths.d.ts +14 -0
  138. package/workspace/paths.js +50 -0
  139. package/workspace/utils.d.ts +8 -0
  140. package/workspace/utils.js +36 -0
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePlanFile = parsePlanFile;
4
+ exports.resolveReference = resolveReference;
5
+ exports.parsePlanFileSimple = parsePlanFileSimple;
6
+ exports.getChanges = getChanges;
7
+ exports.getLatestChange = getLatestChange;
8
+ const fs_1 = require("fs");
9
+ const validators_1 = require("./validators");
10
+ const types_1 = require("@pgpmjs/types");
11
+ /**
12
+ * Parse a Sqitch plan file with full validation
13
+ * Supports both changes and tags
14
+ */
15
+ function parsePlanFile(planPath) {
16
+ const content = (0, fs_1.readFileSync)(planPath, 'utf-8');
17
+ const lines = content.split('\n');
18
+ let project = '';
19
+ let uri = '';
20
+ const changes = [];
21
+ const tags = [];
22
+ const errors = [];
23
+ for (let i = 0; i < lines.length; i++) {
24
+ const line = lines[i];
25
+ const lineNum = i + 1;
26
+ const trimmed = line.trim();
27
+ // Skip empty lines
28
+ if (!trimmed)
29
+ continue;
30
+ // Skip comments
31
+ if (trimmed.startsWith('#'))
32
+ continue;
33
+ // Parse package metadata
34
+ if (trimmed.startsWith('%project=')) {
35
+ project = trimmed.substring('%project='.length);
36
+ continue;
37
+ }
38
+ if (trimmed.startsWith('%uri=')) {
39
+ uri = trimmed.substring('%uri='.length);
40
+ continue;
41
+ }
42
+ // Skip other metadata lines
43
+ if (trimmed.startsWith('%')) {
44
+ continue;
45
+ }
46
+ // Parse tag lines
47
+ if (trimmed.startsWith('@')) {
48
+ const lastChangeName = changes.length > 0 ? changes[changes.length - 1].name : null;
49
+ const tag = parseTagLine(trimmed, lastChangeName);
50
+ if (tag) {
51
+ if ((0, validators_1.isValidTagName)(tag.name)) {
52
+ tags.push(tag);
53
+ }
54
+ else {
55
+ errors.push({
56
+ line: lineNum,
57
+ message: `Invalid tag name: ${tag.name}`
58
+ });
59
+ }
60
+ }
61
+ else {
62
+ errors.push({
63
+ line: lineNum,
64
+ message: 'Invalid tag line format'
65
+ });
66
+ }
67
+ continue;
68
+ }
69
+ // Parse change lines
70
+ const change = parseChangeLine(trimmed);
71
+ if (change) {
72
+ // Validate change name
73
+ if (!(0, validators_1.isValidChangeName)(change.name)) {
74
+ errors.push({
75
+ line: lineNum,
76
+ message: `Invalid change name: ${change.name}`
77
+ });
78
+ continue;
79
+ }
80
+ // Validate dependencies
81
+ for (const dep of change.dependencies) {
82
+ if (!(0, validators_1.isValidDependency)(dep)) {
83
+ errors.push({
84
+ line: lineNum,
85
+ message: `Invalid dependency reference: ${dep}`
86
+ });
87
+ }
88
+ }
89
+ changes.push(change);
90
+ }
91
+ else if (trimmed) {
92
+ // Non-empty line that couldn't be parsed
93
+ errors.push({
94
+ line: lineNum,
95
+ message: 'Invalid line format'
96
+ });
97
+ }
98
+ }
99
+ if (errors.length > 0) {
100
+ return { errors };
101
+ }
102
+ return {
103
+ data: { package: project, uri, changes, tags },
104
+ errors: []
105
+ };
106
+ }
107
+ /**
108
+ * Parse a single change line from a plan file
109
+ * Format: change_name [deps] timestamp planner <email> # comment
110
+ */
111
+ function parseChangeLine(line) {
112
+ // More flexible regex that handles various formats, including planner names with spaces
113
+ // Format: change_name [deps] timestamp planner <email> # comment
114
+ // The timestamp is required if planner/email/comment are present
115
+ const regex = /^(\S+)(?:\s+\[([^\]]*)\])?(?:\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)(?:\s+([^<]+?))?(?:\s+<([^>]+)>)?(?:\s+#\s+(.*))?)?$/;
116
+ const match = line.match(regex);
117
+ if (!match) {
118
+ return null;
119
+ }
120
+ const [, name, depsStr, timestamp, planner, email, comment] = match;
121
+ // Parse dependencies
122
+ const dependencies = depsStr
123
+ ? depsStr.split(/\s+/).filter(dep => dep.length > 0)
124
+ : [];
125
+ return {
126
+ name,
127
+ dependencies,
128
+ timestamp,
129
+ planner,
130
+ email,
131
+ comment
132
+ };
133
+ }
134
+ /**
135
+ * Parse a tag line from a plan file
136
+ * Format: @tag_name change_name timestamp planner <email> # comment
137
+ */
138
+ function parseTagLine(line, lastChangeName) {
139
+ // Tag lines start with @
140
+ if (!line.startsWith('@')) {
141
+ return null;
142
+ }
143
+ // Remove the @ and parse
144
+ const tagContent = line.substring(1);
145
+ // Two possible formats:
146
+ // 1. tagname timestamp planner <email> # comment (tag for last change)
147
+ // 2. tagname changename timestamp planner <email> # comment (tag for specific change)
148
+ // First try to match with change name (format 2)
149
+ const regexWithChange = /^(\S+)\s+(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/;
150
+ let match = tagContent.match(regexWithChange);
151
+ if (match) {
152
+ const [, name, secondToken, timestamp, planner, email, comment] = match;
153
+ // Check if the second token is a timestamp (format 1) or change name (format 2)
154
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(secondToken)) {
155
+ // Format 1: no change name specified
156
+ return {
157
+ name,
158
+ change: lastChangeName || '', // Tag is associated with the last change
159
+ timestamp: secondToken,
160
+ planner: timestamp, // What we thought was timestamp is actually planner
161
+ email: planner, // Shift everything
162
+ comment: email || comment
163
+ };
164
+ }
165
+ else {
166
+ // Format 2: explicit change name
167
+ return {
168
+ name,
169
+ change: secondToken,
170
+ timestamp,
171
+ planner,
172
+ email,
173
+ comment
174
+ };
175
+ }
176
+ }
177
+ // Try simple format without change name
178
+ const regexSimple = /^(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/;
179
+ match = tagContent.match(regexSimple);
180
+ if (!match) {
181
+ return null;
182
+ }
183
+ const [, name, timestamp, planner, email, comment] = match;
184
+ return {
185
+ name,
186
+ change: lastChangeName || '', // Tag is associated with the last change
187
+ timestamp,
188
+ planner,
189
+ email,
190
+ comment
191
+ };
192
+ }
193
+ /**
194
+ * Resolve a reference within a plan file context
195
+ * Handles symbolic references (HEAD, ROOT) and relative references
196
+ */
197
+ function resolveReference(ref, plan, currentPackage) {
198
+ const parsed = (0, validators_1.parseReference)(ref);
199
+ if (!parsed) {
200
+ return { error: `Invalid reference: ${ref}` };
201
+ }
202
+ // Handle package qualifier
203
+ const planPackage = currentPackage || plan.package;
204
+ if (parsed.package && parsed.package !== planPackage) {
205
+ // Cross-package reference - return as-is
206
+ return { change: ref };
207
+ }
208
+ // Handle SHA1 references
209
+ if (parsed.sha1) {
210
+ // Would need to look up in database
211
+ return { change: ref };
212
+ }
213
+ // Handle symbolic references
214
+ if (parsed.symbolic === 'HEAD' && plan.changes.length > 0) {
215
+ return { change: plan.changes[plan.changes.length - 1].name };
216
+ }
217
+ if (parsed.symbolic === 'ROOT' && plan.changes.length > 0) {
218
+ return { change: plan.changes[0].name };
219
+ }
220
+ // Handle relative references
221
+ if (parsed.relative) {
222
+ const baseResolved = resolveReference(parsed.relative.base, plan, currentPackage);
223
+ if (baseResolved.error) {
224
+ return baseResolved;
225
+ }
226
+ // Get the change name from the resolved reference
227
+ const baseChange = baseResolved.change;
228
+ if (!baseChange) {
229
+ return { error: `Cannot resolve base reference: ${parsed.relative.base}` };
230
+ }
231
+ const changeIndex = plan.changes.findIndex(c => c.name === baseChange);
232
+ if (changeIndex === -1) {
233
+ return { error: `Change not found: ${baseChange}` };
234
+ }
235
+ let targetIndex;
236
+ if (parsed.relative.direction === '^') {
237
+ // Go backwards
238
+ targetIndex = changeIndex - parsed.relative.count;
239
+ }
240
+ else {
241
+ // Go forwards
242
+ targetIndex = changeIndex + parsed.relative.count;
243
+ }
244
+ if (targetIndex < 0 || targetIndex >= plan.changes.length) {
245
+ return { error: `Relative reference out of bounds: ${ref}` };
246
+ }
247
+ return { change: plan.changes[targetIndex].name };
248
+ }
249
+ // Handle tag references
250
+ if (parsed.tag) {
251
+ const tag = plan.tags.find(t => t.name === parsed.tag);
252
+ if (!tag) {
253
+ return { error: `Tag not found: ${parsed.tag}` };
254
+ }
255
+ return { tag: parsed.tag, change: tag.change };
256
+ }
257
+ // Handle change@tag format
258
+ if (parsed.change && parsed.tag) {
259
+ return { change: parsed.change, tag: parsed.tag };
260
+ }
261
+ // Plain change reference
262
+ if (parsed.change) {
263
+ // Validate that the change exists in the plan
264
+ const changeExists = plan.changes.some(c => c.name === parsed.change);
265
+ if (!changeExists) {
266
+ return { error: `Change not found: ${parsed.change}` };
267
+ }
268
+ return { change: parsed.change };
269
+ }
270
+ return { error: `Cannot resolve reference: ${ref}` };
271
+ }
272
+ /**
273
+ * Simple plan file parser without validation (for backwards compatibility)
274
+ */
275
+ function parsePlanFileSimple(planPath) {
276
+ const result = parsePlanFile(planPath);
277
+ if (result.data) {
278
+ // Return without tags for simple format
279
+ const { tags, ...planWithoutTags } = result.data;
280
+ return planWithoutTags;
281
+ }
282
+ // If there are errors, throw with details
283
+ if (result.errors && result.errors.length > 0) {
284
+ const errorMessages = result.errors.map(e => `Line ${e.line}: ${e.message}`).join('\n');
285
+ throw types_1.errors.PLAN_PARSE_ERROR({ planPath, errors: errorMessages });
286
+ }
287
+ // Return empty plan if no data and no errors
288
+ return { package: '', uri: '', changes: [] };
289
+ }
290
+ /**
291
+ * Get all change names from a plan file
292
+ */
293
+ function getChanges(planPath) {
294
+ const plan = parsePlanFileSimple(planPath);
295
+ return plan.changes.map(change => change.name);
296
+ }
297
+ /**
298
+ * Get the latest (last) change from a plan file
299
+ */
300
+ function getLatestChange(planPath) {
301
+ const changes = getChanges(planPath);
302
+ return changes[changes.length - 1] || '';
303
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Validators for Sqitch change and tag names according to the spec:
3
+ * https://sqitch.org/docs/manual/sqitchchanges/
4
+ */
5
+ /**
6
+ * Validate a change name according to Sqitch rules:
7
+ * - ≥1 character
8
+ * - Cannot contain whitespace
9
+ * - First and last characters must not be punctuation (except _)
10
+ * - Must not end in ~, ^, /, = or % followed by digits
11
+ * - Disallowed characters anywhere: :, @, #, \
12
+ */
13
+ export declare function isValidChangeName(name: string): boolean;
14
+ /**
15
+ * Validate a tag name according to Sqitch rules:
16
+ * - Same as change name rules
17
+ * - Additionally, must not contain /
18
+ */
19
+ export declare function isValidTagName(name: string): boolean;
20
+ /**
21
+ * Parse a reference that might include project qualifier and/or tag
22
+ * Examples:
23
+ * - "users_table"
24
+ * - "@v1.0"
25
+ * - "users_table@v1.0"
26
+ * - "otherproj:users_table"
27
+ * - "otherproj:@v1.2"
28
+ * - "otherproj:users_table@v1.2"
29
+ * - "40763784148fa190d75bad036730ef44d1c2eac6"
30
+ * - "project:40763784148fa190d75bad036730ef44d1c2eac6"
31
+ */
32
+ export interface ParsedReference {
33
+ package?: string;
34
+ change?: string;
35
+ tag?: string;
36
+ sha1?: string;
37
+ symbolic?: 'HEAD' | 'ROOT';
38
+ relative?: {
39
+ base: string;
40
+ direction: '^' | '~';
41
+ count: number;
42
+ };
43
+ }
44
+ /**
45
+ * Parse a reference string into its components
46
+ */
47
+ export declare function parseReference(ref: string): ParsedReference | null;
48
+ /**
49
+ * Validate a dependency reference
50
+ * Dependencies can be any valid reference format
51
+ */
52
+ export declare function isValidDependency(dep: string): boolean;
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * Validators for Sqitch change and tag names according to the spec:
4
+ * https://sqitch.org/docs/manual/sqitchchanges/
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.isValidChangeName = isValidChangeName;
8
+ exports.isValidTagName = isValidTagName;
9
+ exports.parseReference = parseReference;
10
+ exports.isValidDependency = isValidDependency;
11
+ /**
12
+ * Check if a character is punctuation (excluding underscore)
13
+ */
14
+ function isPunctuation(char) {
15
+ // Common punctuation marks, excluding underscore
16
+ const punctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^`{|}~]/;
17
+ return punctuation.test(char);
18
+ }
19
+ /**
20
+ * Validate a change name according to Sqitch rules:
21
+ * - ≥1 character
22
+ * - Cannot contain whitespace
23
+ * - First and last characters must not be punctuation (except _)
24
+ * - Must not end in ~, ^, /, = or % followed by digits
25
+ * - Disallowed characters anywhere: :, @, #, \
26
+ */
27
+ function isValidChangeName(name) {
28
+ if (!name || name.length === 0) {
29
+ return false;
30
+ }
31
+ // Check for whitespace
32
+ if (/\s/.test(name)) {
33
+ return false;
34
+ }
35
+ // Check for disallowed characters anywhere
36
+ if (/[:@#\\]/.test(name)) {
37
+ return false;
38
+ }
39
+ // Check first character - must not be punctuation except underscore
40
+ const firstChar = name[0];
41
+ if (firstChar !== '_' && isPunctuation(firstChar)) {
42
+ return false;
43
+ }
44
+ // Check last character - must not be punctuation except underscore
45
+ const lastChar = name[name.length - 1];
46
+ if (lastChar !== '_' && isPunctuation(lastChar)) {
47
+ return false;
48
+ }
49
+ // Check for endings like ~N, ^N, /N, =N, %N where N is digits
50
+ if (/[~^/=%]\d+$/.test(name)) {
51
+ return false;
52
+ }
53
+ return true;
54
+ }
55
+ /**
56
+ * Validate a tag name according to Sqitch rules:
57
+ * - Same as change name rules
58
+ * - Additionally, must not contain /
59
+ */
60
+ function isValidTagName(name) {
61
+ if (!isValidChangeName(name)) {
62
+ return false;
63
+ }
64
+ // Tags must not contain /
65
+ if (name.includes('/')) {
66
+ return false;
67
+ }
68
+ return true;
69
+ }
70
+ /**
71
+ * Check if a string is a valid SHA1 hash (40 hex characters)
72
+ */
73
+ function isSHA1(str) {
74
+ return /^[0-9a-f]{40}$/i.test(str);
75
+ }
76
+ /**
77
+ * Parse a reference string into its components
78
+ */
79
+ function parseReference(ref) {
80
+ if (!ref) {
81
+ return null;
82
+ }
83
+ const result = {};
84
+ // Check for package qualifier
85
+ let workingRef = ref;
86
+ const colonIndex = ref.indexOf(':');
87
+ if (colonIndex > 0) {
88
+ const projectPart = ref.substring(0, colonIndex);
89
+ const remainingPart = ref.substring(colonIndex + 1);
90
+ // Check if this is actually a package qualifier or just a colon in the name
91
+ // Package names should be valid identifiers (alphanumeric, dash, underscore)
92
+ // Also check that the remaining part doesn't have more colons
93
+ if (/^[a-zA-Z0-9_-]+$/.test(projectPart) &&
94
+ remainingPart.length > 0 &&
95
+ remainingPart.indexOf(':') === -1) {
96
+ result.package = projectPart;
97
+ workingRef = remainingPart;
98
+ }
99
+ else {
100
+ // Not a valid package qualifier, treat the whole thing as a reference
101
+ workingRef = ref;
102
+ }
103
+ }
104
+ // Check for symbolic references
105
+ if (workingRef === 'HEAD' || workingRef === '@HEAD') {
106
+ result.symbolic = 'HEAD';
107
+ return result;
108
+ }
109
+ if (workingRef === 'ROOT' || workingRef === '@ROOT') {
110
+ result.symbolic = 'ROOT';
111
+ return result;
112
+ }
113
+ // Check for relative references (e.g., HEAD^, @beta~2, HEAD^^)
114
+ // Handle multiple ^ characters
115
+ const multiCaretMatch = workingRef.match(/^(.+?)(\^+)$/);
116
+ if (multiCaretMatch) {
117
+ const [, base, carets] = multiCaretMatch;
118
+ result.relative = {
119
+ base: base, // Keep the original base including @ prefix
120
+ direction: '^',
121
+ count: carets.length
122
+ };
123
+ return result;
124
+ }
125
+ // Handle ^N or ~N format
126
+ const relativeMatch = workingRef.match(/^(.+?)([~^])(\d+)$/);
127
+ if (relativeMatch) {
128
+ const [, base, direction, countStr] = relativeMatch;
129
+ const count = parseInt(countStr, 10);
130
+ result.relative = {
131
+ base: base, // Keep the original base including @ prefix
132
+ direction: direction,
133
+ count
134
+ };
135
+ return result;
136
+ }
137
+ // Handle single ^ or ~ without number
138
+ const singleRelativeMatch = workingRef.match(/^(.+?)([~^])$/);
139
+ if (singleRelativeMatch) {
140
+ const [, base, direction] = singleRelativeMatch;
141
+ result.relative = {
142
+ base: base, // Keep the original base including @ prefix
143
+ direction: direction,
144
+ count: 1
145
+ };
146
+ return result;
147
+ }
148
+ // Check if it's a SHA1
149
+ if (isSHA1(workingRef)) {
150
+ result.sha1 = workingRef;
151
+ return result;
152
+ }
153
+ // Check for tag reference (starts with @)
154
+ if (workingRef.startsWith('@')) {
155
+ const tagName = workingRef.substring(1);
156
+ if (isValidTagName(tagName)) {
157
+ result.tag = tagName;
158
+ return result;
159
+ }
160
+ return null; // Invalid tag name
161
+ }
162
+ // Check for change@tag format
163
+ const atIndex = workingRef.indexOf('@');
164
+ if (atIndex > 0) {
165
+ const changeName = workingRef.substring(0, atIndex);
166
+ const tagName = workingRef.substring(atIndex + 1);
167
+ if (isValidChangeName(changeName) && isValidTagName(tagName)) {
168
+ result.change = changeName;
169
+ result.tag = tagName;
170
+ return result;
171
+ }
172
+ return null; // Invalid change or tag name
173
+ }
174
+ // Must be a plain change name
175
+ if (isValidChangeName(workingRef)) {
176
+ result.change = workingRef;
177
+ return result;
178
+ }
179
+ return null; // Invalid reference
180
+ }
181
+ /**
182
+ * Validate a dependency reference
183
+ * Dependencies can be any valid reference format
184
+ */
185
+ function isValidDependency(dep) {
186
+ return parseReference(dep) !== null;
187
+ }
@@ -0,0 +1,27 @@
1
+ import { Change, SqitchRow, Tag, ExtendedPlanFile } from '../types';
2
+ export interface PlanWriteOptions {
3
+ outdir: string;
4
+ name: string;
5
+ replacer: (str: string) => string;
6
+ author?: string;
7
+ }
8
+ /**
9
+ * Write a Sqitch plan file based on the provided rows
10
+ */
11
+ export declare function writeSqitchPlan(rows: SqitchRow[], opts: PlanWriteOptions): void;
12
+ /**
13
+ * Write a plan file with the provided content
14
+ */
15
+ export declare function writePlanFile(planPath: string, plan: ExtendedPlanFile): void;
16
+ /**
17
+ * Generate content for a plan file
18
+ */
19
+ export declare function generatePlanFileContent(plan: ExtendedPlanFile): string;
20
+ /**
21
+ * Generate a line for a change in a plan file
22
+ */
23
+ export declare function generateChangeLineContent(change: Change): string;
24
+ /**
25
+ * Generate a line for a tag in a plan file
26
+ */
27
+ export declare function generateTagLineContent(tag: Tag): string;
@@ -0,0 +1,124 @@
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.writeSqitchPlan = writeSqitchPlan;
7
+ exports.writePlanFile = writePlanFile;
8
+ exports.generatePlanFileContent = generatePlanFileContent;
9
+ exports.generateChangeLineContent = generateChangeLineContent;
10
+ exports.generateTagLineContent = generateTagLineContent;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ /**
14
+ * Write a Sqitch plan file based on the provided rows
15
+ */
16
+ function writeSqitchPlan(rows, opts) {
17
+ const dir = path_1.default.resolve(path_1.default.join(opts.outdir, opts.name));
18
+ fs_1.default.mkdirSync(dir, { recursive: true });
19
+ const date = () => '2017-08-11T08:11:51Z'; // stubbed timestamp
20
+ const author = opts.author || 'launchql';
21
+ const email = `${author}@5b0c196eeb62`;
22
+ const duplicates = {};
23
+ const plan = opts.replacer(`%syntax-version=1.0.0
24
+ %project=launchql-extension-name
25
+ %uri=launchql-extension-name
26
+
27
+ ${rows
28
+ .map((row) => {
29
+ if (duplicates[row.deploy]) {
30
+ console.log('DUPLICATE ' + row.deploy);
31
+ return '';
32
+ }
33
+ duplicates[row.deploy] = true;
34
+ if (row.deps?.length) {
35
+ return `${row.deploy} [${row.deps.join(' ')}] ${date()} ${author} <${email}> # add ${row.name}`;
36
+ }
37
+ return `${row.deploy} ${date()} ${author} <${email}> # add ${row.name}`;
38
+ })
39
+ .join('\n')}
40
+ `);
41
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'pgpm.plan'), plan);
42
+ }
43
+ /**
44
+ * Write a plan file with the provided content
45
+ */
46
+ function writePlanFile(planPath, plan) {
47
+ const content = generatePlanFileContent(plan);
48
+ fs_1.default.writeFileSync(planPath, content);
49
+ }
50
+ /**
51
+ * Generate content for a plan file
52
+ */
53
+ function generatePlanFileContent(plan) {
54
+ const { package: packageName, uri, changes, tags } = plan;
55
+ let content = `%syntax-version=1.0.0\n`;
56
+ content += `%project=${packageName}\n`;
57
+ if (uri) {
58
+ content += `%uri=${uri}\n`;
59
+ }
60
+ content += `\n`;
61
+ // Add changes and their associated tags
62
+ for (const change of changes) {
63
+ content += generateChangeLineContent(change);
64
+ content += `\n`;
65
+ const associatedTags = tags.filter(tag => tag.change === change.name);
66
+ for (const tag of associatedTags) {
67
+ content += generateTagLineContent(tag);
68
+ content += `\n`;
69
+ }
70
+ }
71
+ return content;
72
+ }
73
+ /**
74
+ * Generate a line for a change in a plan file
75
+ */
76
+ function generateChangeLineContent(change) {
77
+ const { name, dependencies, timestamp, planner, email, comment } = change;
78
+ let line = name;
79
+ // Add dependencies if present
80
+ if (dependencies && dependencies.length > 0) {
81
+ line += ` [${dependencies.join(' ')}]`;
82
+ }
83
+ // Add timestamp if present
84
+ if (timestamp) {
85
+ line += ` ${timestamp}`;
86
+ // Add planner if present
87
+ if (planner) {
88
+ line += ` ${planner}`;
89
+ // Add email if present
90
+ if (email) {
91
+ line += ` <${email}>`;
92
+ }
93
+ }
94
+ }
95
+ // Add comment if present
96
+ if (comment) {
97
+ line += ` # ${comment}`;
98
+ }
99
+ return line;
100
+ }
101
+ /**
102
+ * Generate a line for a tag in a plan file
103
+ */
104
+ function generateTagLineContent(tag) {
105
+ const { name, timestamp, planner, email, comment } = tag;
106
+ let line = `@${name}`;
107
+ // Add timestamp if present
108
+ if (timestamp) {
109
+ line += ` ${timestamp}`;
110
+ // Add planner if present
111
+ if (planner) {
112
+ line += ` ${planner}`;
113
+ // Add email if present
114
+ if (email) {
115
+ line += ` <${email}>`;
116
+ }
117
+ }
118
+ }
119
+ // Add comment if present
120
+ if (comment) {
121
+ line += ` # ${comment}`;
122
+ }
123
+ return line;
124
+ }
@@ -0,0 +1 @@
1
+ export * from './writer';