@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.
- package/CHANGELOG.md +43 -0
- package/README.md +48 -174
- package/docs/2026-10-06-development.md +101 -0
- package/docs/2026-10-06-diffs-and-patches.md +136 -0
- package/docs/2026-10-06-discovery.md +135 -0
- package/docs/2026-10-06-integrations.md +49 -0
- package/docs/2026-10-06-merging.md +153 -0
- package/docs/2026-10-06-safety-and-receipts.md +95 -0
- package/docs/2026-10-06-setup.md +101 -0
- package/docs/2026-10-06-shelvesets-and-reviews.md +113 -0
- package/docs/2026-10-06-status.md +130 -0
- package/docs/2026-10-06-tools-and-loading.md +64 -0
- package/docs/2026-10-06-workspace-operations.md +142 -0
- package/docs/README.md +65 -0
- package/extensions/bash-cm-diff-guard.ts +1 -1
- package/index.ts +4 -687
- package/package.json +10 -4
- package/skills/using-plastic/SKILL.md +11 -5
- package/skills/using-plastic/references/branch-management.md +5 -1
- package/skills/using-plastic/references/changeset-operations.md +9 -8
- package/skills/using-plastic/references/code-review-creation.md +25 -18
- package/skills/using-plastic/references/integration.md +9 -5
- package/skills/using-plastic/references/quick-reference.md +4 -4
- package/skills/using-plastic/references/reviewing-changes.md +10 -10
- package/skills/using-plastic/references/shelving.md +26 -11
- package/skills/using-plastic/references/troubleshooting.md +4 -4
- package/src/diff/local-snapshot.ts +74 -0
- package/src/diff/text.ts +187 -0
- package/src/domain/add-contract.ts +24 -0
- package/src/domain/branch-create-contract.ts +38 -0
- package/src/domain/branch-delete-contract.ts +15 -0
- package/src/domain/branches.ts +263 -0
- package/src/domain/checkin-contract.ts +70 -0
- package/src/domain/closeout-contract.ts +30 -0
- package/src/domain/copied-merge.ts +42 -0
- package/src/domain/diff-base-xml.ts +65 -0
- package/src/domain/diff-pending-xml.ts +34 -0
- package/src/domain/merge-output.ts +327 -0
- package/src/domain/paths.ts +253 -0
- package/src/domain/pending.ts +499 -0
- package/src/domain/removal-contract.ts +25 -0
- package/src/domain/revisions.ts +301 -0
- package/src/domain/server-merge-contract.ts +45 -0
- package/src/domain/server-merge-resolutions.ts +61 -0
- package/src/domain/status-xml.ts +122 -0
- package/src/domain/switch-contract.ts +60 -0
- package/src/domain/switch-policy.ts +95 -0
- package/src/domain/undo-contract.ts +24 -0
- package/src/domain/update-contract.ts +16 -0
- package/src/domain/workspace-merge-contract.ts +41 -0
- package/src/execution/add-command.ts +118 -0
- package/src/execution/branch-create-command.ts +118 -0
- package/src/execution/branch-list-command.ts +75 -0
- package/src/execution/checkin-command.ts +116 -0
- package/src/execution/cli-version.ts +31 -0
- package/src/execution/cm.ts +59 -0
- package/src/execution/code-review-find-command.ts +75 -0
- package/src/execution/context.ts +20 -0
- package/src/execution/diff-revisions-command.ts +74 -0
- package/src/execution/process.ts +260 -0
- package/src/execution/removal-command.ts +118 -0
- package/src/execution/server-merge-command.ts +92 -0
- package/src/execution/shelveset-list-command.ts +75 -0
- package/src/execution/status-command.ts +29 -0
- package/src/execution/status-xml-command.ts +72 -0
- package/src/execution/status-xml-limits.ts +1 -0
- package/src/execution/switch-command.ts +118 -0
- package/src/execution/test-seams.ts +10 -0
- package/src/execution/undo-command.ts +118 -0
- package/src/execution/update-command.ts +118 -0
- package/src/execution/workspace-list-command.ts +75 -0
- package/src/execution/workspace-merge-command.ts +118 -0
- package/src/operations/add-receipt.ts +37 -0
- package/src/operations/arguments.ts +3 -0
- package/src/operations/branch-create.ts +74 -0
- package/src/operations/branch-delete-receipt.ts +25 -0
- package/src/operations/branch-list.ts +94 -0
- package/src/operations/branch-reads.ts +94 -0
- package/src/operations/branches.ts +111 -0
- package/src/operations/checkin-receipt.ts +140 -0
- package/src/operations/checkin.ts +102 -0
- package/src/operations/closeout-receipt.ts +74 -0
- package/src/operations/closeout.ts +19 -0
- package/src/operations/code-review-find.ts +97 -0
- package/src/operations/consolidated-diff.ts +144 -0
- package/src/operations/diff-revisions.ts +157 -0
- package/src/operations/diff.ts +53 -0
- package/src/operations/merge.ts +26 -0
- package/src/operations/object-delete-receipt.ts +21 -0
- package/src/operations/object-write-receipt.ts +105 -0
- package/src/operations/patch-receipt.ts +96 -0
- package/src/operations/patch-support.ts +375 -0
- package/src/operations/patch.ts +25 -0
- package/src/operations/removal-receipt.ts +42 -0
- package/src/operations/reviews.ts +90 -0
- package/src/operations/server-merge.ts +130 -0
- package/src/operations/shelveset-list.ts +90 -0
- package/src/operations/shelvesets.ts +90 -0
- package/src/operations/status.ts +129 -0
- package/src/operations/switch-receipt.ts +85 -0
- package/src/operations/switch.ts +25 -0
- package/src/operations/undo-receipt.ts +37 -0
- package/src/operations/update-receipt.ts +35 -0
- package/src/operations/workspace-list.ts +92 -0
- package/src/operations/workspace-merge-receipt.ts +50 -0
- package/src/operations/workspace.ts +120 -0
- package/src/pi/add-output.ts +52 -0
- package/src/pi/arguments.ts +298 -0
- package/src/pi/branch-create-output.ts +83 -0
- package/src/pi/branch-delete-output.ts +46 -0
- package/src/pi/branch-list-output.ts +59 -0
- package/src/pi/branch-output.ts +63 -0
- package/src/pi/capability-diagnostics.ts +23 -0
- package/src/pi/checkin-output.ts +68 -0
- package/src/pi/closeout-output.ts +87 -0
- package/src/pi/code-review-find-output.ts +50 -0
- package/src/pi/diff-output.ts +66 -0
- package/src/pi/diff-revisions-output.ts +55 -0
- package/src/pi/object-delete-output.ts +15 -0
- package/src/pi/object-write-output.ts +54 -0
- package/src/pi/patch-output.ts +69 -0
- package/src/pi/register.ts +188 -0
- package/src/pi/removal-output.ts +59 -0
- package/src/pi/schemas.ts +146 -0
- package/src/pi/server-merge-output.ts +83 -0
- package/src/pi/shelveset-list-output.ts +50 -0
- package/src/pi/status-output.ts +204 -0
- package/src/pi/switch-output.ts +80 -0
- package/src/pi/tool-registry.ts +72 -0
- package/src/pi/undo-output.ts +52 -0
- package/src/pi/update-output.ts +48 -0
- package/src/pi/workspace-list-output.ts +53 -0
- package/src/pi/workspace-merge-output.ts +46 -0
- package/src/plastic-core.ts +14 -5775
- package/src/plastic-renderers.ts +102 -22
- package/src/plastic-tool-loading.ts +7 -12
- package/src/presentation/add-results.ts +5 -0
- package/src/presentation/branch-create.ts +7 -0
- package/src/presentation/branch-delete-results.ts +7 -0
- package/src/presentation/checkin-results.ts +14 -0
- package/src/presentation/closeout-results.ts +2 -0
- package/src/presentation/diff-results.ts +237 -0
- package/src/presentation/removal-results.ts +7 -0
- package/src/presentation/results.ts +84 -0
- package/src/presentation/switch-results.ts +7 -0
- package/src/presentation/undo-results.ts +5 -0
- package/src/presentation/update-results.ts +5 -0
- 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
|
+
};
|