ppcos 1.0.2 → 1.0.3
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/lib/commands/status.js +3 -2
- package/lib/commands/update.js +95 -8
- package/lib/utils/fs-helpers.js +2 -2
- package/lib/utils/manifest.js +2 -1
- package/package.json +1 -1
package/lib/commands/status.js
CHANGED
|
@@ -85,8 +85,9 @@ function findCustomSkills(clientDir, manifest) {
|
|
|
85
85
|
// Get managed skill directories from manifest
|
|
86
86
|
const managedSkillDirs = new Set();
|
|
87
87
|
for (const path of Object.keys(manifest.managedFiles)) {
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const normalized = path.replace(/\\/g, '/');
|
|
89
|
+
if (normalized.startsWith('.claude/skills/')) {
|
|
90
|
+
const parts = normalized.split('/');
|
|
90
91
|
if (parts.length >= 3) {
|
|
91
92
|
managedSkillDirs.add(parts[2]);
|
|
92
93
|
}
|
package/lib/commands/update.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync } from 'node:fs';
|
|
8
|
-
import { join, dirname } from 'node:path';
|
|
8
|
+
import { join, dirname, sep } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { createInterface } from 'node:readline';
|
|
11
11
|
import {
|
|
@@ -105,6 +105,50 @@ function migrateSettingsDenyRules(clientDir) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* One-time migration: normalize backslash manifest keys to forward slashes.
|
|
110
|
+
* On Windows, getAllFiles() returned backslash paths which were stored as manifest keys.
|
|
111
|
+
* This normalizes them so lookups work cross-platform.
|
|
112
|
+
*/
|
|
113
|
+
function migrateManifestKeys(manifest) {
|
|
114
|
+
let fixed = 0;
|
|
115
|
+
for (const [key, value] of Object.entries(manifest.managedFiles)) {
|
|
116
|
+
const normalized = key.replace(/\\/g, '/');
|
|
117
|
+
if (normalized !== key) {
|
|
118
|
+
delete manifest.managedFiles[key];
|
|
119
|
+
manifest.managedFiles[normalized] = value;
|
|
120
|
+
fixed++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const conflict of (manifest.conflicts || [])) {
|
|
124
|
+
if (conflict.file) {
|
|
125
|
+
conflict.file = conflict.file.replace(/\\/g, '/');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (fixed > 0) {
|
|
129
|
+
logger.info(` Normalized ${fixed} manifest key${fixed !== 1 ? 's' : ''}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* One-time migration: fix managedType for config files.
|
|
135
|
+
* On Windows, isConfigFile() failed due to backslash paths, so config files
|
|
136
|
+
* got stored with managedType 'update' instead of 'config'.
|
|
137
|
+
* This corrects existing manifests in-place (before detectModifications runs).
|
|
138
|
+
*/
|
|
139
|
+
function migrateConfigManagedTypes(manifest) {
|
|
140
|
+
let fixed = 0;
|
|
141
|
+
for (const [path, info] of Object.entries(manifest.managedFiles)) {
|
|
142
|
+
if (isConfigFile(path) && info.managedType !== 'config') {
|
|
143
|
+
info.managedType = 'config';
|
|
144
|
+
fixed++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (fixed > 0) {
|
|
148
|
+
logger.info(` Fixed managedType for ${fixed} config file${fixed !== 1 ? 's' : ''}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
/**
|
|
109
153
|
* Find all client directories with .managed.json
|
|
110
154
|
* @returns {string[]} Array of client names
|
|
@@ -220,6 +264,21 @@ async function backupFiles(clientDir, files) {
|
|
|
220
264
|
const { writeFile } = await import('node:fs/promises');
|
|
221
265
|
await writeFile(join(backupDir, 'manifest.txt'), manifestContent);
|
|
222
266
|
|
|
267
|
+
// Verify all files were actually backed up before returning
|
|
268
|
+
const { readFileSync } = await import('node:fs');
|
|
269
|
+
for (const relativePath of files) {
|
|
270
|
+
const backedUpPath = join(backupDir, relativePath);
|
|
271
|
+
if (!existsSync(backedUpPath)) {
|
|
272
|
+
throw new Error(`Backup verification failed: ${relativePath} was not created in ${backupDir}`);
|
|
273
|
+
}
|
|
274
|
+
// Verify content matches source (guards against empty/placeholder files from cloud sync)
|
|
275
|
+
const srcContent = readFileSync(join(clientDir, relativePath));
|
|
276
|
+
const backupContent = readFileSync(backedUpPath);
|
|
277
|
+
if (!srcContent.equals(backupContent)) {
|
|
278
|
+
throw new Error(`Backup verification failed: ${relativePath} content mismatch (possible cloud sync interference)`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
223
282
|
return backupDir;
|
|
224
283
|
}
|
|
225
284
|
|
|
@@ -252,6 +311,8 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
252
311
|
// Migrations for existing clients
|
|
253
312
|
await ensureMemoryFolder(clientDir);
|
|
254
313
|
migrateSettingsDenyRules(clientDir);
|
|
314
|
+
migrateManifestKeys(manifest);
|
|
315
|
+
migrateConfigManagedTypes(manifest);
|
|
255
316
|
|
|
256
317
|
// Check if already up to date
|
|
257
318
|
if (currentVersion === packageVersion) {
|
|
@@ -315,9 +376,16 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
315
376
|
}
|
|
316
377
|
|
|
317
378
|
if (resolution === 'backup') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
379
|
+
try {
|
|
380
|
+
const backupDir = await backupFiles(clientDir, modifiedNonOrphans);
|
|
381
|
+
backedUpFiles = modifiedNonOrphans;
|
|
382
|
+
const relativePath = backupDir.replace(clientDir + sep, '');
|
|
383
|
+
console.log(` Backed up ${modifiedNonOrphans.length} modified files to ${relativePath}`);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
logger.error(` Backup failed: ${err.message}`);
|
|
386
|
+
logger.error(' Update aborted — no files were overwritten.');
|
|
387
|
+
return { status: 'cancelled' };
|
|
388
|
+
}
|
|
321
389
|
}
|
|
322
390
|
}
|
|
323
391
|
|
|
@@ -345,16 +413,28 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
345
413
|
const srcPath = join(basePath, relativePath);
|
|
346
414
|
const destPath = join(clientDir, relativePath);
|
|
347
415
|
|
|
416
|
+
const srcChecksum = await calculateChecksum(srcPath);
|
|
348
417
|
await copyFileWithDirs(srcPath, destPath);
|
|
349
418
|
|
|
350
|
-
|
|
419
|
+
// Verify copy integrity (guards against cloud sync placeholder files)
|
|
420
|
+
let destChecksum = await calculateChecksum(destPath);
|
|
421
|
+
if (destChecksum !== srcChecksum) {
|
|
422
|
+
// Retry once — cloud sync may have interfered
|
|
423
|
+
await copyFileWithDirs(srcPath, destPath);
|
|
424
|
+
destChecksum = await calculateChecksum(destPath);
|
|
425
|
+
if (destChecksum !== srcChecksum) {
|
|
426
|
+
logger.error(` File write failed for ${relativePath} (possible cloud sync interference).`);
|
|
427
|
+
logger.error(' Update aborted — try running update again.');
|
|
428
|
+
return { status: 'cancelled' };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
351
431
|
|
|
352
432
|
// Preserve managedType from existing manifest or determine new
|
|
353
433
|
const existingEntry = manifest.managedFiles[relativePath];
|
|
354
434
|
const managedType = existingEntry?.managedType || getManagedType(relativePath);
|
|
355
435
|
|
|
356
436
|
updatedFiles[relativePath] = {
|
|
357
|
-
checksum,
|
|
437
|
+
checksum: srcChecksum,
|
|
358
438
|
version: packageVersion,
|
|
359
439
|
managedType
|
|
360
440
|
};
|
|
@@ -366,8 +446,15 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
366
446
|
// Back up modified orphans before removing
|
|
367
447
|
const modifiedOrphans = orphanedFiles.filter(f => mods.modified.includes(f));
|
|
368
448
|
if (modifiedOrphans.length > 0) {
|
|
369
|
-
|
|
370
|
-
|
|
449
|
+
try {
|
|
450
|
+
const backupDir = await backupFiles(clientDir, modifiedOrphans);
|
|
451
|
+
const relPath = backupDir.replace(clientDir + sep, '');
|
|
452
|
+
console.log(` Backed up ${modifiedOrphans.length} modified removed file${modifiedOrphans.length !== 1 ? 's' : ''} to ${relPath}`);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
logger.error(` Backup of removed files failed: ${err.message}`);
|
|
455
|
+
logger.error(' Update aborted — no files were removed.');
|
|
456
|
+
return { status: 'cancelled' };
|
|
457
|
+
}
|
|
371
458
|
}
|
|
372
459
|
|
|
373
460
|
for (const relativePath of orphanedFiles) {
|
package/lib/utils/fs-helpers.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function getAllFiles(dir, baseDir = dir) {
|
|
|
45
45
|
if (entry.isDirectory()) {
|
|
46
46
|
files.push(...await getAllFiles(fullPath, baseDir));
|
|
47
47
|
} else {
|
|
48
|
-
files.push(relative(baseDir, fullPath));
|
|
48
|
+
files.push(relative(baseDir, fullPath).replace(/\\/g, '/'));
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -67,7 +67,7 @@ export function getAllFilesSync(dir, baseDir = dir) {
|
|
|
67
67
|
if (entry.isDirectory()) {
|
|
68
68
|
files.push(...getAllFilesSync(fullPath, baseDir));
|
|
69
69
|
} else {
|
|
70
|
-
files.push(relative(baseDir, fullPath));
|
|
70
|
+
files.push(relative(baseDir, fullPath).replace(/\\/g, '/'));
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
package/lib/utils/manifest.js
CHANGED
|
@@ -25,7 +25,8 @@ const CONFIG_ONLY_FILES = new Set([
|
|
|
25
25
|
* @returns {boolean}
|
|
26
26
|
*/
|
|
27
27
|
export function isConfigFile(relativePath) {
|
|
28
|
-
|
|
28
|
+
// Normalize backslashes to forward slashes for Windows compatibility
|
|
29
|
+
return CONFIG_ONLY_FILES.has(relativePath.replace(/\\/g, '/'));
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|