@aefree/pi-plastic 0.7.6 → 0.8.1

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 (148) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +48 -174
  3. package/docs/2026-10-06-development.md +101 -0
  4. package/docs/2026-10-06-diffs-and-patches.md +136 -0
  5. package/docs/2026-10-06-discovery.md +135 -0
  6. package/docs/2026-10-06-integrations.md +49 -0
  7. package/docs/2026-10-06-merging.md +153 -0
  8. package/docs/2026-10-06-safety-and-receipts.md +95 -0
  9. package/docs/2026-10-06-setup.md +101 -0
  10. package/docs/2026-10-06-shelvesets-and-reviews.md +113 -0
  11. package/docs/2026-10-06-status.md +130 -0
  12. package/docs/2026-10-06-tools-and-loading.md +64 -0
  13. package/docs/2026-10-06-workspace-operations.md +142 -0
  14. package/docs/README.md +65 -0
  15. package/extensions/bash-cm-diff-guard.ts +1 -1
  16. package/index.ts +4 -687
  17. package/package.json +10 -4
  18. package/skills/using-plastic/SKILL.md +11 -5
  19. package/skills/using-plastic/references/branch-management.md +5 -1
  20. package/skills/using-plastic/references/changeset-operations.md +9 -8
  21. package/skills/using-plastic/references/code-review-creation.md +25 -18
  22. package/skills/using-plastic/references/integration.md +9 -5
  23. package/skills/using-plastic/references/quick-reference.md +4 -4
  24. package/skills/using-plastic/references/reviewing-changes.md +10 -10
  25. package/skills/using-plastic/references/shelving.md +26 -11
  26. package/skills/using-plastic/references/troubleshooting.md +4 -4
  27. package/src/diff/local-snapshot.ts +74 -0
  28. package/src/diff/text.ts +187 -0
  29. package/src/domain/add-contract.ts +24 -0
  30. package/src/domain/branch-create-contract.ts +38 -0
  31. package/src/domain/branch-delete-contract.ts +15 -0
  32. package/src/domain/branches.ts +263 -0
  33. package/src/domain/checkin-contract.ts +70 -0
  34. package/src/domain/closeout-contract.ts +30 -0
  35. package/src/domain/copied-merge.ts +42 -0
  36. package/src/domain/diff-base-xml.ts +65 -0
  37. package/src/domain/diff-pending-xml.ts +34 -0
  38. package/src/domain/merge-output.ts +327 -0
  39. package/src/domain/paths.ts +253 -0
  40. package/src/domain/pending.ts +499 -0
  41. package/src/domain/removal-contract.ts +25 -0
  42. package/src/domain/revisions.ts +301 -0
  43. package/src/domain/server-merge-contract.ts +45 -0
  44. package/src/domain/server-merge-resolutions.ts +61 -0
  45. package/src/domain/status-xml.ts +122 -0
  46. package/src/domain/switch-contract.ts +60 -0
  47. package/src/domain/switch-policy.ts +95 -0
  48. package/src/domain/undo-contract.ts +24 -0
  49. package/src/domain/update-contract.ts +16 -0
  50. package/src/domain/workspace-merge-contract.ts +41 -0
  51. package/src/execution/add-command.ts +118 -0
  52. package/src/execution/branch-create-command.ts +118 -0
  53. package/src/execution/branch-list-command.ts +75 -0
  54. package/src/execution/checkin-command.ts +116 -0
  55. package/src/execution/cli-version.ts +31 -0
  56. package/src/execution/cm.ts +59 -0
  57. package/src/execution/code-review-find-command.ts +75 -0
  58. package/src/execution/context.ts +20 -0
  59. package/src/execution/diff-revisions-command.ts +74 -0
  60. package/src/execution/process.ts +260 -0
  61. package/src/execution/removal-command.ts +118 -0
  62. package/src/execution/server-merge-command.ts +92 -0
  63. package/src/execution/shelveset-list-command.ts +75 -0
  64. package/src/execution/status-command.ts +29 -0
  65. package/src/execution/status-xml-command.ts +72 -0
  66. package/src/execution/status-xml-limits.ts +1 -0
  67. package/src/execution/switch-command.ts +118 -0
  68. package/src/execution/test-seams.ts +10 -0
  69. package/src/execution/undo-command.ts +118 -0
  70. package/src/execution/update-command.ts +118 -0
  71. package/src/execution/workspace-list-command.ts +75 -0
  72. package/src/execution/workspace-merge-command.ts +118 -0
  73. package/src/operations/add-receipt.ts +37 -0
  74. package/src/operations/arguments.ts +3 -0
  75. package/src/operations/branch-create.ts +74 -0
  76. package/src/operations/branch-delete-receipt.ts +25 -0
  77. package/src/operations/branch-list.ts +94 -0
  78. package/src/operations/branch-reads.ts +94 -0
  79. package/src/operations/branches.ts +111 -0
  80. package/src/operations/checkin-receipt.ts +140 -0
  81. package/src/operations/checkin.ts +102 -0
  82. package/src/operations/closeout-receipt.ts +74 -0
  83. package/src/operations/closeout.ts +19 -0
  84. package/src/operations/code-review-find.ts +97 -0
  85. package/src/operations/consolidated-diff.ts +144 -0
  86. package/src/operations/diff-revisions.ts +157 -0
  87. package/src/operations/diff.ts +53 -0
  88. package/src/operations/merge.ts +26 -0
  89. package/src/operations/object-delete-receipt.ts +21 -0
  90. package/src/operations/object-write-receipt.ts +105 -0
  91. package/src/operations/patch-receipt.ts +96 -0
  92. package/src/operations/patch-support.ts +375 -0
  93. package/src/operations/patch.ts +25 -0
  94. package/src/operations/removal-receipt.ts +42 -0
  95. package/src/operations/reviews.ts +90 -0
  96. package/src/operations/server-merge.ts +130 -0
  97. package/src/operations/shelveset-list.ts +90 -0
  98. package/src/operations/shelvesets.ts +90 -0
  99. package/src/operations/status.ts +129 -0
  100. package/src/operations/switch-receipt.ts +85 -0
  101. package/src/operations/switch.ts +25 -0
  102. package/src/operations/undo-receipt.ts +37 -0
  103. package/src/operations/update-receipt.ts +35 -0
  104. package/src/operations/workspace-list.ts +92 -0
  105. package/src/operations/workspace-merge-receipt.ts +50 -0
  106. package/src/operations/workspace.ts +120 -0
  107. package/src/pi/add-output.ts +52 -0
  108. package/src/pi/arguments.ts +298 -0
  109. package/src/pi/branch-create-output.ts +83 -0
  110. package/src/pi/branch-delete-output.ts +46 -0
  111. package/src/pi/branch-list-output.ts +59 -0
  112. package/src/pi/branch-output.ts +63 -0
  113. package/src/pi/capability-diagnostics.ts +23 -0
  114. package/src/pi/checkin-output.ts +68 -0
  115. package/src/pi/closeout-output.ts +87 -0
  116. package/src/pi/code-review-find-output.ts +50 -0
  117. package/src/pi/diff-output.ts +66 -0
  118. package/src/pi/diff-revisions-output.ts +55 -0
  119. package/src/pi/object-delete-output.ts +15 -0
  120. package/src/pi/object-write-output.ts +54 -0
  121. package/src/pi/patch-output.ts +69 -0
  122. package/src/pi/register.ts +188 -0
  123. package/src/pi/removal-output.ts +59 -0
  124. package/src/pi/schemas.ts +146 -0
  125. package/src/pi/server-merge-output.ts +83 -0
  126. package/src/pi/shelveset-list-output.ts +50 -0
  127. package/src/pi/status-output.ts +204 -0
  128. package/src/pi/switch-output.ts +80 -0
  129. package/src/pi/tool-registry.ts +72 -0
  130. package/src/pi/undo-output.ts +52 -0
  131. package/src/pi/update-output.ts +48 -0
  132. package/src/pi/workspace-list-output.ts +53 -0
  133. package/src/pi/workspace-merge-output.ts +46 -0
  134. package/src/plastic-core.ts +14 -5775
  135. package/src/plastic-renderers.ts +102 -22
  136. package/src/plastic-tool-loading.ts +7 -12
  137. package/src/presentation/add-results.ts +5 -0
  138. package/src/presentation/branch-create.ts +7 -0
  139. package/src/presentation/branch-delete-results.ts +7 -0
  140. package/src/presentation/checkin-results.ts +14 -0
  141. package/src/presentation/closeout-results.ts +2 -0
  142. package/src/presentation/diff-results.ts +237 -0
  143. package/src/presentation/removal-results.ts +7 -0
  144. package/src/presentation/results.ts +84 -0
  145. package/src/presentation/switch-results.ts +7 -0
  146. package/src/presentation/undo-results.ts +5 -0
  147. package/src/presentation/update-results.ts +5 -0
  148. package/src/presentation/workspace-merge-results.ts +6 -0
@@ -0,0 +1,253 @@
1
+ import { promises as fs, realpathSync, statSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, win32 } from "path";
3
+
4
+ export const isWindowsStyleAbsolutePath = (absolutePath: string): boolean => /^[A-Za-z]:\//.test(absolutePath) || absolutePath.startsWith("//");
5
+
6
+ export const toNormalizedAbsolutePath = (pathValue: string, cwd: string): string =>
7
+ {
8
+ const normalizedPathValue = pathValue.replace(/\\/g, "/");
9
+ const normalizedCwd = cwd.replace(/\\/g, "/");
10
+ if (isWindowsStyleAbsolutePath(normalizedPathValue))
11
+ {
12
+ return win32.normalize(normalizedPathValue).replace(/\\/g, "/");
13
+ }
14
+
15
+ if (isWindowsStyleAbsolutePath(normalizedCwd))
16
+ {
17
+ return win32.resolve(normalizedCwd, normalizedPathValue).replace(/\\/g, "/");
18
+ }
19
+
20
+ const absolutePath = isAbsolute(pathValue) ? resolve(pathValue) : resolve(cwd, pathValue);
21
+ return absolutePath.replace(/\\/g, "/");
22
+ };
23
+
24
+ export const darwinVolumeCaseSensitivity = new Map<string, boolean>();
25
+ export const isSameFilesystemDevice = (volumeDevice: number | bigint, candidateDevice: number | bigint): boolean =>
26
+ volumeDevice === candidateDevice;
27
+
28
+ export const findCanonicalExistingAncestor = (absolutePath: string): string | null =>
29
+ {
30
+ let candidate = absolutePath;
31
+ while (true)
32
+ {
33
+ try
34
+ {
35
+ return realpathSync.native(candidate).replace(/\\/g, "/");
36
+ }
37
+ catch
38
+ {
39
+ const parent = dirname(candidate);
40
+ if (parent === candidate)
41
+ {
42
+ return null;
43
+ }
44
+
45
+ candidate = parent;
46
+ }
47
+ }
48
+ };
49
+
50
+ export const toFilesystemIdentityPath = async (absolutePath: string): Promise<string> =>
51
+ {
52
+ let candidate = absolutePath;
53
+ const missingSegments: string[] = [];
54
+ while (true)
55
+ {
56
+ try
57
+ {
58
+ const canonicalAncestor = (await fs.realpath(candidate)).replace(/\\/g, "/");
59
+ return missingSegments.reduce((identityPath, segment) => join(identityPath, segment), canonicalAncestor).replace(/\\/g, "/");
60
+ }
61
+ catch (error)
62
+ {
63
+ const code = (error as NodeJS.ErrnoException).code;
64
+ if (code !== "ENOENT" && code !== "ENOTDIR")
65
+ {
66
+ throw error;
67
+ }
68
+
69
+ const parent = dirname(candidate);
70
+ if (parent === candidate)
71
+ {
72
+ throw error;
73
+ }
74
+
75
+ missingSegments.unshift(basename(candidate));
76
+ candidate = parent;
77
+ }
78
+ }
79
+ };
80
+
81
+ export const toggleFirstAsciiLetterCase = (value: string): string | null =>
82
+ {
83
+ const index = value.search(/[A-Za-z]/);
84
+ if (index < 0)
85
+ {
86
+ return null;
87
+ }
88
+
89
+ const letter = value[index];
90
+ const toggled = letter === letter.toLowerCase() ? letter.toUpperCase() : letter.toLowerCase();
91
+ return `${value.slice(0, index)}${toggled}${value.slice(index + 1)}`;
92
+ };
93
+
94
+ // APFS may be formatted case-sensitive or case-insensitive, so Darwin must probe the
95
+ // canonical existing ancestor on the target volume instead of assuming a platform policy.
96
+ export const isDarwinCaseInsensitiveVolume = (absolutePath: string): boolean =>
97
+ {
98
+ const canonicalAncestor = findCanonicalExistingAncestor(absolutePath);
99
+ if (!canonicalAncestor)
100
+ {
101
+ return false;
102
+ }
103
+
104
+ try
105
+ {
106
+ const volumeDevice = statSync(canonicalAncestor).dev;
107
+ const volumeKey = String(volumeDevice);
108
+ const cached = darwinVolumeCaseSensitivity.get(volumeKey);
109
+ if (cached !== undefined)
110
+ {
111
+ return cached;
112
+ }
113
+
114
+ let probePath = canonicalAncestor;
115
+ while (true)
116
+ {
117
+ const parent = dirname(probePath);
118
+ if (parent === probePath)
119
+ {
120
+ darwinVolumeCaseSensitivity.set(volumeKey, false);
121
+ return false;
122
+ }
123
+
124
+ // A mount point's parent belongs to a different filesystem. Do not
125
+ // infer the mounted volume's case policy from the parent/root volume.
126
+ if (!isSameFilesystemDevice(volumeDevice, statSync(parent).dev))
127
+ {
128
+ darwinVolumeCaseSensitivity.set(volumeKey, false);
129
+ return false;
130
+ }
131
+
132
+ const alternateName = toggleFirstAsciiLetterCase(probePath.slice(parent.length + 1));
133
+ if (alternateName)
134
+ {
135
+ try
136
+ {
137
+ const canonicalAlternate = realpathSync.native(join(parent, alternateName)).replace(/\\/g, "/");
138
+ const isCaseInsensitive = canonicalAlternate === probePath;
139
+ darwinVolumeCaseSensitivity.set(volumeKey, isCaseInsensitive);
140
+ return isCaseInsensitive;
141
+ }
142
+ catch
143
+ {
144
+ darwinVolumeCaseSensitivity.set(volumeKey, false);
145
+ return false;
146
+ }
147
+ }
148
+
149
+ probePath = parent;
150
+ }
151
+ }
152
+ catch
153
+ {
154
+ // A failed probe must preserve distinct paths rather than risk conflating files.
155
+ return false;
156
+ }
157
+ };
158
+
159
+ export type PathComparisonPolicy = {
160
+ platform?: NodeJS.Platform;
161
+ isDarwinCaseInsensitiveVolume?: (absolutePath: string) => boolean;
162
+ };
163
+
164
+ export const toPathComparisonKeyFromAbsolutePath = (absolutePath: string, policy: PathComparisonPolicy = {}): string =>
165
+ {
166
+ const normalizedPath = absolutePath.replace(/\\/g, "/");
167
+ // Keep Windows and UNC behavior regardless of the host platform, including when parsing
168
+ // status captured from a Windows workspace on another machine.
169
+ if (isWindowsStyleAbsolutePath(normalizedPath))
170
+ {
171
+ return normalizedPath.toLowerCase();
172
+ }
173
+
174
+ if ((policy.platform ?? process.platform) === "darwin"
175
+ && (policy.isDarwinCaseInsensitiveVolume ?? isDarwinCaseInsensitiveVolume)(normalizedPath))
176
+ {
177
+ return normalizedPath.toLowerCase();
178
+ }
179
+
180
+ return normalizedPath;
181
+ };
182
+
183
+ export const toPathComparisonKey = (pathValue: string, cwd: string): string => toPathComparisonKeyFromAbsolutePath(toNormalizedAbsolutePath(pathValue, cwd));
184
+
185
+ export const toCommandPath = (absolutePath: string, cwd: string): string =>
186
+ {
187
+ const relativePath = isWindowsStyleAbsolutePath(absolutePath.replace(/\\/g, "/"))
188
+ && isWindowsStyleAbsolutePath(cwd.replace(/\\/g, "/"))
189
+ ? win32.relative(cwd, absolutePath)
190
+ : relative(cwd, absolutePath);
191
+ if (relativePath.length === 0)
192
+ {
193
+ return ".";
194
+ }
195
+
196
+ if (relativePath.startsWith(".."))
197
+ {
198
+ return absolutePath;
199
+ }
200
+
201
+ return relativePath;
202
+ };
203
+
204
+ export const isWithinPathScope = (candidatePath: string, scopePath: string): boolean =>
205
+ {
206
+ if (candidatePath === scopePath)
207
+ {
208
+ return true;
209
+ }
210
+
211
+ return candidatePath.startsWith(`${scopePath}/`);
212
+ };
213
+
214
+ export const dedupeAndMinimizeAbsolutePaths = (absolutePaths: string[]): string[] =>
215
+ {
216
+ const uniquePaths: string[] = [];
217
+ const seenComparisonKeys = new Set<string>();
218
+ for (const path of absolutePaths)
219
+ {
220
+ const comparisonKey = toPathComparisonKeyFromAbsolutePath(path);
221
+ if (seenComparisonKeys.has(comparisonKey))
222
+ {
223
+ continue;
224
+ }
225
+
226
+ seenComparisonKeys.add(comparisonKey);
227
+ uniquePaths.push(path);
228
+ }
229
+
230
+ const sortedPaths = uniquePaths.sort((left, right) => left.length - right.length);
231
+ const minimizedPaths: string[] = [];
232
+ const minimizedComparisonKeys: string[] = [];
233
+
234
+ for (const path of sortedPaths)
235
+ {
236
+ const comparisonKey = toPathComparisonKeyFromAbsolutePath(path);
237
+ if (minimizedComparisonKeys.some((scopePath) => isWithinPathScope(comparisonKey, scopePath)))
238
+ {
239
+ continue;
240
+ }
241
+
242
+ minimizedPaths.push(path);
243
+ minimizedComparisonKeys.push(comparisonKey);
244
+ }
245
+
246
+ return minimizedPaths;
247
+ };
248
+
249
+ export const buildFallbackScopePaths = (absolutePaths: string[], cwd: string): string[] =>
250
+ {
251
+ const fallbackAbsolutePaths = absolutePaths.map((path) => toNormalizedAbsolutePath(dirname(path), cwd));
252
+ return dedupeAndMinimizeAbsolutePaths(fallbackAbsolutePaths).map((path) => toCommandPath(path, cwd));
253
+ };