claude-dev-env 2.14.1 → 2.15.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/_shared/advisor/scripts/tier_model_ids.py +6 -2
- package/bin/AGENTS.md +6 -4
- package/bin/install-constants.mjs +51 -0
- package/bin/install.codex-rules.test.mjs +173 -0
- package/bin/install.cursor-rules.test.mjs +103 -0
- package/bin/install.mjs +165 -51
- package/bin/install.profile-root.test.mjs +8 -0
- package/bin/install.prune.test.mjs +2 -1
- package/bin/install.test.mjs +42 -8
- package/bin/install.transaction.test.mjs +1 -0
- package/bin/install.uninstall-transaction.test.mjs +1 -0
- package/bin/resolve-install-root.mjs +43 -10
- package/codex-rules/claude-dev-env.rules +12 -0
- package/package.json +3 -2
- package/scripts/AGENTS.md +0 -7
- package/scripts/claude_chain_runner.py +49 -2
- package/scripts/invoke_code_review.py +48 -51
- package/scripts/resolve_worker_spawn.py +42 -47
- package/scripts/sync_to_cursor/AGENTS.md +3 -3
- package/scripts/sync_to_cursor/canonical_docs.py +11 -11
- package/scripts/sync_to_cursor/config/__init__.py +8 -0
- package/scripts/sync_to_cursor/engine.py +26 -1
- package/scripts/sync_to_cursor/rules.py +76 -5
- package/scripts/test_claude_chain_runner.py +28 -0
- package/scripts/test_dispatcher_profile_import.py +184 -0
- package/scripts/test_resolve_worker_spawn.py +119 -1
- package/scripts/test_validate_instruction_pairs.py +30 -0
- package/scripts/tests/AGENTS.md +2 -0
- package/scripts/tests/test_engine.py +102 -0
- package/scripts/tests/test_rules.py +79 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +0 -25
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +0 -60
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +0 -54
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +0 -64
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +0 -180
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +0 -288
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +0 -275
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +0 -221
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +0 -108
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +0 -147
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +0 -102
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +0 -210
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +0 -299
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +0 -127
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +0 -51
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +0 -77
- package/scripts/sync_to_cursor/config.py +0 -5
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure CLI vs Desktop embedded-version compatibility classifier.
|
|
3
|
-
*
|
|
4
|
-
* The classifier takes probe results only. Callers own process spawn, timeouts,
|
|
5
|
-
* and binary discovery. Fail closed on unreadable or missing inputs.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export const COMPATIBILITY_POLICY_VERSION = 1;
|
|
9
|
-
|
|
10
|
-
/** @typedef {'pass' | 'warn' | 'block'} CompatibilityAction */
|
|
11
|
-
/** @typedef {
|
|
12
|
-
* | 'equal'
|
|
13
|
-
* | 'patch-drift'
|
|
14
|
-
* | 'minor-drift'
|
|
15
|
-
* | 'major-drift'
|
|
16
|
-
* | 'missing-binary'
|
|
17
|
-
* | 'unreadable'
|
|
18
|
-
* | 'process-error'
|
|
19
|
-
* | 'non-semver'
|
|
20
|
-
* } CompatibilityClass */
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Action table for every compatibility class. Missing / unreadable / process
|
|
24
|
-
* errors and major/minor drift block; patch drift warns; equality passes.
|
|
25
|
-
*/
|
|
26
|
-
export const COMPATIBILITY_ACTION_BY_CLASS = Object.freeze({
|
|
27
|
-
equal: 'pass',
|
|
28
|
-
'patch-drift': 'warn',
|
|
29
|
-
'minor-drift': 'block',
|
|
30
|
-
'major-drift': 'block',
|
|
31
|
-
'missing-binary': 'block',
|
|
32
|
-
unreadable: 'block',
|
|
33
|
-
'process-error': 'block',
|
|
34
|
-
'non-semver': 'block',
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @typedef {{
|
|
39
|
-
* path: string | null,
|
|
40
|
-
* versionText: string | null,
|
|
41
|
-
* errorCode?: 'missing' | 'unreadable' | 'process-error' | null,
|
|
42
|
-
* errorMessage?: string | null,
|
|
43
|
-
* }} VersionProbeResult
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @typedef {{
|
|
48
|
-
* cliPath: string | null,
|
|
49
|
-
* desktopPath: string | null,
|
|
50
|
-
* cliVersion: string | null,
|
|
51
|
-
* desktopVersion: string | null,
|
|
52
|
-
* policyVersion: number,
|
|
53
|
-
* class: CompatibilityClass,
|
|
54
|
-
* action: CompatibilityAction,
|
|
55
|
-
* message: string,
|
|
56
|
-
* }} CompatibilityResult
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Classify CLI and Desktop embedded version compatibility from probe results.
|
|
61
|
-
*
|
|
62
|
-
* ::
|
|
63
|
-
*
|
|
64
|
-
* classifyVersionCompatibility({
|
|
65
|
-
* cli: { path: 'cli', versionText: '2.1.220' },
|
|
66
|
-
* desktop: { path: 'desktop', versionText: '2.1.220' },
|
|
67
|
-
* })
|
|
68
|
-
* // class: equal, action: pass
|
|
69
|
-
*
|
|
70
|
-
* classifyVersionCompatibility({
|
|
71
|
-
* cli: { path: 'cli', versionText: '2.1.220' },
|
|
72
|
-
* desktop: { path: 'desktop', versionText: '2.1.219' },
|
|
73
|
-
* })
|
|
74
|
-
* // class: patch-drift, action: warn
|
|
75
|
-
*
|
|
76
|
-
* @param {{
|
|
77
|
-
* cli: VersionProbeResult,
|
|
78
|
-
* desktop: VersionProbeResult,
|
|
79
|
-
* }} parameters
|
|
80
|
-
* @returns {CompatibilityResult}
|
|
81
|
-
*/
|
|
82
|
-
export function classifyVersionCompatibility(parameters) {
|
|
83
|
-
const cliProbe = normalizeProbe(parameters.cli);
|
|
84
|
-
const desktopProbe = normalizeProbe(parameters.desktop);
|
|
85
|
-
|
|
86
|
-
const base = {
|
|
87
|
-
cliPath: cliProbe.path,
|
|
88
|
-
desktopPath: desktopProbe.path,
|
|
89
|
-
cliVersion: extractVersionLabel(cliProbe.versionText),
|
|
90
|
-
desktopVersion: extractVersionLabel(desktopProbe.versionText),
|
|
91
|
-
policyVersion: COMPATIBILITY_POLICY_VERSION,
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const probeFailure = firstProbeFailure(cliProbe, desktopProbe);
|
|
95
|
-
if (probeFailure) {
|
|
96
|
-
return finalizeResult({
|
|
97
|
-
...base,
|
|
98
|
-
class: probeFailure.className,
|
|
99
|
-
message: probeFailure.message,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const cliVersion = parseSemver(cliProbe.versionText);
|
|
104
|
-
const desktopVersion = parseSemver(desktopProbe.versionText);
|
|
105
|
-
if (!cliVersion || !desktopVersion) {
|
|
106
|
-
return finalizeResult({
|
|
107
|
-
...base,
|
|
108
|
-
class: 'non-semver',
|
|
109
|
-
message: formatNonSemverMessage(cliProbe.versionText, desktopProbe.versionText),
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
cliVersion.major === desktopVersion.major
|
|
115
|
-
&& cliVersion.minor === desktopVersion.minor
|
|
116
|
-
&& cliVersion.patch === desktopVersion.patch
|
|
117
|
-
) {
|
|
118
|
-
return finalizeResult({
|
|
119
|
-
...base,
|
|
120
|
-
class: 'equal',
|
|
121
|
-
message: `CLI and Desktop versions match (${formatSemver(cliVersion)})`,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (cliVersion.major !== desktopVersion.major) {
|
|
126
|
-
return finalizeResult({
|
|
127
|
-
...base,
|
|
128
|
-
class: 'major-drift',
|
|
129
|
-
message: formatDriftMessage('major', cliVersion, desktopVersion),
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (cliVersion.minor !== desktopVersion.minor) {
|
|
134
|
-
return finalizeResult({
|
|
135
|
-
...base,
|
|
136
|
-
class: 'minor-drift',
|
|
137
|
-
message: formatDriftMessage('minor', cliVersion, desktopVersion),
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return finalizeResult({
|
|
142
|
-
...base,
|
|
143
|
-
class: 'patch-drift',
|
|
144
|
-
message: formatDriftMessage('patch', cliVersion, desktopVersion),
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* True when the classified action blocks launch before profile-state mutation.
|
|
150
|
-
*
|
|
151
|
-
* @param {CompatibilityResult} result
|
|
152
|
-
* @returns {boolean}
|
|
153
|
-
*/
|
|
154
|
-
export function shouldBlockLaunch(result) {
|
|
155
|
-
return result.action === 'block';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Parse a version string into major.minor.patch when the leading triple is semver-shaped.
|
|
160
|
-
*
|
|
161
|
-
* @param {string | null | undefined} versionText
|
|
162
|
-
* @returns {{major: number, minor: number, patch: number} | null}
|
|
163
|
-
*/
|
|
164
|
-
export function parseSemver(versionText) {
|
|
165
|
-
if (typeof versionText !== 'string') {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
const trimmed = versionText.trim();
|
|
169
|
-
if (!trimmed) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
const match = trimmed.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
173
|
-
if (!match) {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
major: Number(match[1]),
|
|
178
|
-
minor: Number(match[2]),
|
|
179
|
-
patch: Number(match[3]),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* @param {VersionProbeResult | null | undefined} probe
|
|
185
|
-
* @returns {VersionProbeResult}
|
|
186
|
-
*/
|
|
187
|
-
function normalizeProbe(probe) {
|
|
188
|
-
if (!probe || typeof probe !== 'object') {
|
|
189
|
-
return {
|
|
190
|
-
path: null,
|
|
191
|
-
versionText: null,
|
|
192
|
-
errorCode: 'missing',
|
|
193
|
-
errorMessage: 'probe missing',
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
path: typeof probe.path === 'string' ? probe.path : null,
|
|
198
|
-
versionText: typeof probe.versionText === 'string' ? probe.versionText : null,
|
|
199
|
-
errorCode: probe.errorCode ?? null,
|
|
200
|
-
errorMessage: probe.errorMessage ?? null,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* @param {VersionProbeResult} cliProbe
|
|
206
|
-
* @param {VersionProbeResult} desktopProbe
|
|
207
|
-
* @returns {{className: CompatibilityClass, message: string} | null}
|
|
208
|
-
*/
|
|
209
|
-
function firstProbeFailure(cliProbe, desktopProbe) {
|
|
210
|
-
for (const [eachLabel, eachProbe] of [
|
|
211
|
-
['CLI', cliProbe],
|
|
212
|
-
['Desktop', desktopProbe],
|
|
213
|
-
]) {
|
|
214
|
-
if (eachProbe.errorCode === 'missing' || !eachProbe.path) {
|
|
215
|
-
return {
|
|
216
|
-
className: 'missing-binary',
|
|
217
|
-
message: `${eachLabel} binary path is missing`,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
if (eachProbe.errorCode === 'process-error') {
|
|
221
|
-
return {
|
|
222
|
-
className: 'process-error',
|
|
223
|
-
message: `${eachLabel} version probe failed: ${eachProbe.errorMessage || 'process error'}`,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
if (eachProbe.errorCode === 'unreadable' || eachProbe.versionText === null) {
|
|
227
|
-
return {
|
|
228
|
-
className: 'unreadable',
|
|
229
|
-
message: `${eachLabel} version output is unreadable`,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* @param {string | null} versionText
|
|
238
|
-
* @returns {string | null}
|
|
239
|
-
*/
|
|
240
|
-
function extractVersionLabel(versionText) {
|
|
241
|
-
if (typeof versionText !== 'string') {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
const trimmed = versionText.trim();
|
|
245
|
-
return trimmed || null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* @param {{
|
|
250
|
-
* cliPath: string | null,
|
|
251
|
-
* desktopPath: string | null,
|
|
252
|
-
* cliVersion: string | null,
|
|
253
|
-
* desktopVersion: string | null,
|
|
254
|
-
* policyVersion: number,
|
|
255
|
-
* class: CompatibilityClass,
|
|
256
|
-
* message: string,
|
|
257
|
-
* }} partial
|
|
258
|
-
* @returns {CompatibilityResult}
|
|
259
|
-
*/
|
|
260
|
-
function finalizeResult(partial) {
|
|
261
|
-
const action = COMPATIBILITY_ACTION_BY_CLASS[partial.class];
|
|
262
|
-
return {
|
|
263
|
-
cliPath: partial.cliPath,
|
|
264
|
-
desktopPath: partial.desktopPath,
|
|
265
|
-
cliVersion: partial.cliVersion,
|
|
266
|
-
desktopVersion: partial.desktopVersion,
|
|
267
|
-
policyVersion: partial.policyVersion,
|
|
268
|
-
class: partial.class,
|
|
269
|
-
action,
|
|
270
|
-
message: partial.message,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* @param {'major' | 'minor' | 'patch'} axis
|
|
276
|
-
* @param {{major: number, minor: number, patch: number}} cliVersion
|
|
277
|
-
* @param {{major: number, minor: number, patch: number}} desktopVersion
|
|
278
|
-
* @returns {string}
|
|
279
|
-
*/
|
|
280
|
-
function formatDriftMessage(axis, cliVersion, desktopVersion) {
|
|
281
|
-
return `${axis} drift: CLI ${formatSemver(cliVersion)} vs Desktop ${formatSemver(desktopVersion)}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* @param {{major: number, minor: number, patch: number}} version
|
|
286
|
-
* @returns {string}
|
|
287
|
-
*/
|
|
288
|
-
function formatSemver(version) {
|
|
289
|
-
return `${version.major}.${version.minor}.${version.patch}`;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* @param {string | null} cliText
|
|
294
|
-
* @param {string | null} desktopText
|
|
295
|
-
* @returns {string}
|
|
296
|
-
*/
|
|
297
|
-
function formatNonSemverMessage(cliText, desktopText) {
|
|
298
|
-
return `non-semver version text: CLI=${JSON.stringify(cliText)} Desktop=${JSON.stringify(desktopText)}`;
|
|
299
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# Read-only inventory of managed Claude profile shortcuts.
|
|
2
|
-
# Does not mutate Desktop, Start Menu, or taskbar state.
|
|
3
|
-
|
|
4
|
-
[CmdletBinding()]
|
|
5
|
-
param(
|
|
6
|
-
[string]$ManifestPath = (Join-Path $PSScriptRoot 'shortcut-manifest.json'),
|
|
7
|
-
[string]$DesktopPath = [Environment]::GetFolderPath('Desktop'),
|
|
8
|
-
[string]$StartMenuPath = [Environment]::GetFolderPath('StartMenu'),
|
|
9
|
-
[string]$OutputPath
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
Set-StrictMode -Version Latest
|
|
13
|
-
$ErrorActionPreference = 'Stop'
|
|
14
|
-
|
|
15
|
-
if (-not (Test-Path -LiteralPath $ManifestPath)) {
|
|
16
|
-
throw "shortcut manifest missing: $ManifestPath"
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
$manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
|
20
|
-
if ($manifest.schemaVersion -ne 1) {
|
|
21
|
-
throw "unsupported shortcut manifest schemaVersion: $($manifest.schemaVersion)"
|
|
22
|
-
}
|
|
23
|
-
if ($null -eq $manifest.allManagedShortcuts) {
|
|
24
|
-
throw 'shortcut manifest allManagedShortcuts is required'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
$allSeenIds = @{}
|
|
28
|
-
$allSeenVisibleNames = @{}
|
|
29
|
-
$allSeenGrouping = @{}
|
|
30
|
-
foreach ($eachShortcut in $manifest.allManagedShortcuts) {
|
|
31
|
-
foreach ($eachFieldName in @('id', 'visibleName', 'source', 'profileId', 'locationKind', 'groupingIdentity')) {
|
|
32
|
-
$fieldValue = $eachShortcut.$eachFieldName
|
|
33
|
-
if ([string]::IsNullOrWhiteSpace([string]$fieldValue)) {
|
|
34
|
-
throw "shortcut missing required field $eachFieldName"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if ($allSeenIds.ContainsKey($eachShortcut.id)) {
|
|
38
|
-
throw "duplicate shortcut id: $($eachShortcut.id)"
|
|
39
|
-
}
|
|
40
|
-
if ($allSeenVisibleNames.ContainsKey($eachShortcut.visibleName)) {
|
|
41
|
-
throw "duplicate shortcut visibleName: $($eachShortcut.visibleName)"
|
|
42
|
-
}
|
|
43
|
-
if ($allSeenGrouping.ContainsKey($eachShortcut.groupingIdentity)) {
|
|
44
|
-
throw "duplicate shortcut groupingIdentity: $($eachShortcut.groupingIdentity)"
|
|
45
|
-
}
|
|
46
|
-
$allSeenIds[$eachShortcut.id] = $true
|
|
47
|
-
$allSeenVisibleNames[$eachShortcut.visibleName] = $true
|
|
48
|
-
$allSeenGrouping[$eachShortcut.groupingIdentity] = $true
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function Get-ShortcutMetadata {
|
|
52
|
-
param(
|
|
53
|
-
[Parameter(Mandatory = $true)]
|
|
54
|
-
[string]$ShortcutPath,
|
|
55
|
-
[Parameter(Mandatory = $true)]
|
|
56
|
-
$Shell
|
|
57
|
-
)
|
|
58
|
-
if (-not (Test-Path -LiteralPath $ShortcutPath)) {
|
|
59
|
-
return $null
|
|
60
|
-
}
|
|
61
|
-
$link = $Shell.CreateShortcut($ShortcutPath)
|
|
62
|
-
return [pscustomobject]@{
|
|
63
|
-
path = $ShortcutPath
|
|
64
|
-
targetPath = $link.TargetPath
|
|
65
|
-
arguments = $link.Arguments
|
|
66
|
-
workingDirectory = $link.WorkingDirectory
|
|
67
|
-
iconLocation = $link.IconLocation
|
|
68
|
-
exists = $true
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
$shell = New-Object -ComObject WScript.Shell
|
|
73
|
-
$allRows = [System.Collections.Generic.List[object]]::new()
|
|
74
|
-
try {
|
|
75
|
-
foreach ($eachShortcut in $manifest.allManagedShortcuts) {
|
|
76
|
-
if ($eachShortcut.locationKind -eq 'desktop') {
|
|
77
|
-
$baseDirectory = $DesktopPath
|
|
78
|
-
}
|
|
79
|
-
elseif ($eachShortcut.locationKind -eq 'start-menu') {
|
|
80
|
-
$baseDirectory = $StartMenuPath
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
throw "unsupported locationKind for $($eachShortcut.id): $($eachShortcut.locationKind)"
|
|
84
|
-
}
|
|
85
|
-
$shortcutPath = Join-Path $baseDirectory ($eachShortcut.visibleName + '.lnk')
|
|
86
|
-
$metadata = Get-ShortcutMetadata -ShortcutPath $shortcutPath -Shell $shell
|
|
87
|
-
$allRows.Add([pscustomobject]@{
|
|
88
|
-
id = $eachShortcut.id
|
|
89
|
-
visibleName = $eachShortcut.visibleName
|
|
90
|
-
source = $eachShortcut.source
|
|
91
|
-
profileId = $eachShortcut.profileId
|
|
92
|
-
locationKind = $eachShortcut.locationKind
|
|
93
|
-
targetKind = $eachShortcut.targetKind
|
|
94
|
-
launcherName = $eachShortcut.launcherName
|
|
95
|
-
groupingIdentity = $eachShortcut.groupingIdentity
|
|
96
|
-
expectedPath = $shortcutPath
|
|
97
|
-
exists = [bool]$metadata
|
|
98
|
-
targetPath = if ($metadata) { $metadata.targetPath } else { $null }
|
|
99
|
-
arguments = if ($metadata) { $metadata.arguments } else { $null }
|
|
100
|
-
workingDirectory = if ($metadata) { $metadata.workingDirectory } else { $null }
|
|
101
|
-
iconLocation = if ($metadata) { $metadata.iconLocation } else { $null }
|
|
102
|
-
liveMutationAuthorized = [bool]$manifest.policy.liveMutationAuthorized
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
$result = [pscustomobject]@{
|
|
111
|
-
mode = 'read-only-inventory'
|
|
112
|
-
policy = $manifest.policy
|
|
113
|
-
scannedAt = (Get-Date).ToString('o')
|
|
114
|
-
desktopPath = $DesktopPath
|
|
115
|
-
startMenuPath = $StartMenuPath
|
|
116
|
-
shortcuts = $allRows
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
$json = $result | ConvertTo-Json -Depth 6
|
|
120
|
-
if ($OutputPath) {
|
|
121
|
-
$directory = Split-Path -Parent $OutputPath
|
|
122
|
-
if ($directory -and -not (Test-Path -LiteralPath $directory)) {
|
|
123
|
-
New-Item -ItemType Directory -Path $directory | Out-Null
|
|
124
|
-
}
|
|
125
|
-
[IO.File]::WriteAllText($OutputPath, $json, [Text.UTF8Encoding]::new($false))
|
|
126
|
-
}
|
|
127
|
-
Write-Output $json
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": 1,
|
|
3
|
-
"policy": {
|
|
4
|
-
"unsuffixedNames": "Native Desktop wins: Claude Profile A and Claude Profile B",
|
|
5
|
-
"browserNames": "Chrome variants use explicit Web or Chrome source labels",
|
|
6
|
-
"authority": "The owner confirms or revises this policy before live mutation.",
|
|
7
|
-
"liveMutationAuthorized": false
|
|
8
|
-
},
|
|
9
|
-
"allManagedShortcuts": [
|
|
10
|
-
{
|
|
11
|
-
"id": "desktop-profile-a-native",
|
|
12
|
-
"visibleName": "Claude Profile A",
|
|
13
|
-
"source": "native-desktop",
|
|
14
|
-
"profileId": "profile-a",
|
|
15
|
-
"locationKind": "desktop",
|
|
16
|
-
"targetKind": "native-launcher",
|
|
17
|
-
"launcherName": "claude-profile-a",
|
|
18
|
-
"groupingIdentity": "native-profile-a"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "desktop-profile-b-native",
|
|
22
|
-
"visibleName": "Claude Profile B",
|
|
23
|
-
"source": "native-desktop",
|
|
24
|
-
"profileId": "profile-b",
|
|
25
|
-
"locationKind": "desktop",
|
|
26
|
-
"targetKind": "native-launcher",
|
|
27
|
-
"launcherName": "claude-profile-b",
|
|
28
|
-
"groupingIdentity": "native-profile-b"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"id": "start-profile-a-chrome",
|
|
32
|
-
"visibleName": "Claude Profile A (Chrome)",
|
|
33
|
-
"source": "chrome-start-menu",
|
|
34
|
-
"profileId": "profile-a",
|
|
35
|
-
"locationKind": "start-menu",
|
|
36
|
-
"targetKind": "chrome-profile",
|
|
37
|
-
"launcherName": "claude-profile-a",
|
|
38
|
-
"groupingIdentity": "chrome-profile-a"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"id": "start-profile-b-chrome",
|
|
42
|
-
"visibleName": "Claude Profile B (Chrome)",
|
|
43
|
-
"source": "chrome-start-menu",
|
|
44
|
-
"profileId": "profile-b",
|
|
45
|
-
"locationKind": "start-menu",
|
|
46
|
-
"targetKind": "chrome-profile",
|
|
47
|
-
"launcherName": "claude-profile-b",
|
|
48
|
-
"groupingIdentity": "chrome-profile-b"
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# Read-only reconciliation preview for managed Claude profile shortcuts.
|
|
2
|
-
# Live mutation is refused while policy.liveMutationAuthorized is false.
|
|
3
|
-
|
|
4
|
-
[CmdletBinding()]
|
|
5
|
-
param(
|
|
6
|
-
[string]$ManifestPath = (Join-Path $PSScriptRoot 'shortcut-manifest.json'),
|
|
7
|
-
[string]$DesktopPath = [Environment]::GetFolderPath('Desktop'),
|
|
8
|
-
[string]$StartMenuPath = [Environment]::GetFolderPath('StartMenu'),
|
|
9
|
-
[string]$OutputPath,
|
|
10
|
-
[switch]$Apply
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
Set-StrictMode -Version Latest
|
|
14
|
-
$ErrorActionPreference = 'Stop'
|
|
15
|
-
|
|
16
|
-
$inventoryScript = Join-Path $PSScriptRoot 'shortcut-inventory.ps1'
|
|
17
|
-
if (-not (Test-Path -LiteralPath $inventoryScript)) {
|
|
18
|
-
throw "inventory script missing: $inventoryScript"
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
$manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
|
22
|
-
$inventoryJson = & $inventoryScript -ManifestPath $ManifestPath -DesktopPath $DesktopPath -StartMenuPath $StartMenuPath
|
|
23
|
-
$inventory = $inventoryJson | ConvertFrom-Json
|
|
24
|
-
|
|
25
|
-
$allActions = [System.Collections.Generic.List[object]]::new()
|
|
26
|
-
foreach ($eachRow in $inventory.shortcuts) {
|
|
27
|
-
$actualPresent = [bool]$eachRow.exists
|
|
28
|
-
$hasSourceIdentity = -not [string]::IsNullOrWhiteSpace([string]$eachRow.source) -and
|
|
29
|
-
-not [string]::IsNullOrWhiteSpace([string]$eachRow.profileId) -and
|
|
30
|
-
-not [string]::IsNullOrWhiteSpace([string]$eachRow.groupingIdentity)
|
|
31
|
-
$status = if (-not $actualPresent) {
|
|
32
|
-
'missing'
|
|
33
|
-
}
|
|
34
|
-
elseif ($hasSourceIdentity) {
|
|
35
|
-
'present-unverified-target'
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
'source-ambiguous'
|
|
39
|
-
}
|
|
40
|
-
$allActions.Add([pscustomobject]@{
|
|
41
|
-
id = $eachRow.id
|
|
42
|
-
visibleName = $eachRow.visibleName
|
|
43
|
-
source = $eachRow.source
|
|
44
|
-
profileId = $eachRow.profileId
|
|
45
|
-
groupingIdentity = $eachRow.groupingIdentity
|
|
46
|
-
desiredPresent = $true
|
|
47
|
-
actualPresent = $actualPresent
|
|
48
|
-
status = $status
|
|
49
|
-
expectedPath = $eachRow.expectedPath
|
|
50
|
-
mutation = 'none'
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if ($Apply) {
|
|
55
|
-
if (-not [bool]$manifest.policy.liveMutationAuthorized) {
|
|
56
|
-
throw 'live shortcut mutation refused: policy.liveMutationAuthorized is false; owner confirmation required'
|
|
57
|
-
}
|
|
58
|
-
throw 'live shortcut mutation adapter is residual after owner confirmation (N1 apply path)'
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
$preview = [pscustomobject]@{
|
|
62
|
-
mode = 'preview-only'
|
|
63
|
-
policy = $manifest.policy
|
|
64
|
-
actionCount = $allActions.Count
|
|
65
|
-
actions = $allActions
|
|
66
|
-
liveMutationAuthorized = [bool]$manifest.policy.liveMutationAuthorized
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
$json = $preview | ConvertTo-Json -Depth 6
|
|
70
|
-
if ($OutputPath) {
|
|
71
|
-
$directory = Split-Path -Parent $OutputPath
|
|
72
|
-
if ($directory -and -not (Test-Path -LiteralPath $directory)) {
|
|
73
|
-
New-Item -ItemType Directory -Path $directory | Out-Null
|
|
74
|
-
}
|
|
75
|
-
[IO.File]::WriteAllText($OutputPath, $json, [Text.UTF8Encoding]::new($false))
|
|
76
|
-
}
|
|
77
|
-
Write-Output $json
|