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.
@@ -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
- if (path.startsWith('.claude/skills/')) {
89
- const parts = path.split('/');
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
  }
@@ -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
- const backupDir = await backupFiles(clientDir, modifiedNonOrphans);
319
- backedUpFiles = modifiedNonOrphans;
320
- console.log(` Backed up ${modifiedNonOrphans.length} modified files to ${backupDir.replace(clientDir + '/', '')}`);
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
- const checksum = await calculateChecksum(destPath);
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
- const backupDir = await backupFiles(clientDir, modifiedOrphans);
370
- console.log(` Backed up ${modifiedOrphans.length} modified removed file${modifiedOrphans.length !== 1 ? 's' : ''} to ${backupDir.replace(clientDir + '/', '')}`);
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) {
@@ -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
 
@@ -25,7 +25,8 @@ const CONFIG_ONLY_FILES = new Set([
25
25
  * @returns {boolean}
26
26
  */
27
27
  export function isConfigFile(relativePath) {
28
- return CONFIG_ONLY_FILES.has(relativePath);
28
+ // Normalize backslashes to forward slashes for Windows compatibility
29
+ return CONFIG_ONLY_FILES.has(relativePath.replace(/\\/g, '/'));
29
30
  }
30
31
 
31
32
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ppcos",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "CLI tool to manage Google Ads AI workflow skills and agents for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {