ppcos 1.0.2 → 1.0.4
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 +165 -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,119 @@ function migrateSettingsDenyRules(clientDir) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* One-time migration: merge hooks config into settings.local.json
|
|
110
|
+
* Adds missing hook events without touching existing ones
|
|
111
|
+
*/
|
|
112
|
+
function migrateSettingsHooks(clientDir) {
|
|
113
|
+
const settingsPath = join(clientDir, '.claude', 'settings.local.json');
|
|
114
|
+
if (!existsSync(settingsPath)) return;
|
|
115
|
+
|
|
116
|
+
const requiredHooks = {
|
|
117
|
+
SessionStart: [
|
|
118
|
+
{
|
|
119
|
+
matcher: 'startup|resume|clear',
|
|
120
|
+
hooks: [
|
|
121
|
+
{
|
|
122
|
+
type: 'command',
|
|
123
|
+
command: '.claude/hooks/session-context-check.sh',
|
|
124
|
+
timeout: 30,
|
|
125
|
+
statusMessage: 'Checking context freshness...'
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
Stop: [
|
|
131
|
+
{
|
|
132
|
+
hooks: [
|
|
133
|
+
{
|
|
134
|
+
type: 'command',
|
|
135
|
+
command: '.claude/hooks/stop-memory-check.sh',
|
|
136
|
+
timeout: 10,
|
|
137
|
+
statusMessage: 'Checking memory log...'
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
PostCompact: [
|
|
143
|
+
{
|
|
144
|
+
hooks: [
|
|
145
|
+
{
|
|
146
|
+
type: 'command',
|
|
147
|
+
command: '.claude/hooks/post-compact-reminder.sh',
|
|
148
|
+
timeout: 30,
|
|
149
|
+
statusMessage: 'Restoring context after compaction...'
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
158
|
+
if (!settings.hooks) settings.hooks = {};
|
|
159
|
+
|
|
160
|
+
const added = [];
|
|
161
|
+
for (const [event, config] of Object.entries(requiredHooks)) {
|
|
162
|
+
if (!settings.hooks[event]) {
|
|
163
|
+
settings.hooks[event] = config;
|
|
164
|
+
added.push(event);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (added.length === 0) return;
|
|
169
|
+
|
|
170
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
171
|
+
logger.info(` Added ${added.length} hook${added.length !== 1 ? 's' : ''} to settings.local.json (${added.join(', ')})`);
|
|
172
|
+
} catch {
|
|
173
|
+
// Don't fail update over migration
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* One-time migration: normalize backslash manifest keys to forward slashes.
|
|
179
|
+
* On Windows, getAllFiles() returned backslash paths which were stored as manifest keys.
|
|
180
|
+
* This normalizes them so lookups work cross-platform.
|
|
181
|
+
*/
|
|
182
|
+
function migrateManifestKeys(manifest) {
|
|
183
|
+
let fixed = 0;
|
|
184
|
+
for (const [key, value] of Object.entries(manifest.managedFiles)) {
|
|
185
|
+
const normalized = key.replace(/\\/g, '/');
|
|
186
|
+
if (normalized !== key) {
|
|
187
|
+
delete manifest.managedFiles[key];
|
|
188
|
+
manifest.managedFiles[normalized] = value;
|
|
189
|
+
fixed++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const conflict of (manifest.conflicts || [])) {
|
|
193
|
+
if (conflict.file) {
|
|
194
|
+
conflict.file = conflict.file.replace(/\\/g, '/');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (fixed > 0) {
|
|
198
|
+
logger.info(` Normalized ${fixed} manifest key${fixed !== 1 ? 's' : ''}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* One-time migration: fix managedType for config files.
|
|
204
|
+
* On Windows, isConfigFile() failed due to backslash paths, so config files
|
|
205
|
+
* got stored with managedType 'update' instead of 'config'.
|
|
206
|
+
* This corrects existing manifests in-place (before detectModifications runs).
|
|
207
|
+
*/
|
|
208
|
+
function migrateConfigManagedTypes(manifest) {
|
|
209
|
+
let fixed = 0;
|
|
210
|
+
for (const [path, info] of Object.entries(manifest.managedFiles)) {
|
|
211
|
+
if (isConfigFile(path) && info.managedType !== 'config') {
|
|
212
|
+
info.managedType = 'config';
|
|
213
|
+
fixed++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (fixed > 0) {
|
|
217
|
+
logger.info(` Fixed managedType for ${fixed} config file${fixed !== 1 ? 's' : ''}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
108
221
|
/**
|
|
109
222
|
* Find all client directories with .managed.json
|
|
110
223
|
* @returns {string[]} Array of client names
|
|
@@ -220,6 +333,21 @@ async function backupFiles(clientDir, files) {
|
|
|
220
333
|
const { writeFile } = await import('node:fs/promises');
|
|
221
334
|
await writeFile(join(backupDir, 'manifest.txt'), manifestContent);
|
|
222
335
|
|
|
336
|
+
// Verify all files were actually backed up before returning
|
|
337
|
+
const { readFileSync } = await import('node:fs');
|
|
338
|
+
for (const relativePath of files) {
|
|
339
|
+
const backedUpPath = join(backupDir, relativePath);
|
|
340
|
+
if (!existsSync(backedUpPath)) {
|
|
341
|
+
throw new Error(`Backup verification failed: ${relativePath} was not created in ${backupDir}`);
|
|
342
|
+
}
|
|
343
|
+
// Verify content matches source (guards against empty/placeholder files from cloud sync)
|
|
344
|
+
const srcContent = readFileSync(join(clientDir, relativePath));
|
|
345
|
+
const backupContent = readFileSync(backedUpPath);
|
|
346
|
+
if (!srcContent.equals(backupContent)) {
|
|
347
|
+
throw new Error(`Backup verification failed: ${relativePath} content mismatch (possible cloud sync interference)`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
223
351
|
return backupDir;
|
|
224
352
|
}
|
|
225
353
|
|
|
@@ -252,6 +380,9 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
252
380
|
// Migrations for existing clients
|
|
253
381
|
await ensureMemoryFolder(clientDir);
|
|
254
382
|
migrateSettingsDenyRules(clientDir);
|
|
383
|
+
migrateSettingsHooks(clientDir);
|
|
384
|
+
migrateManifestKeys(manifest);
|
|
385
|
+
migrateConfigManagedTypes(manifest);
|
|
255
386
|
|
|
256
387
|
// Check if already up to date
|
|
257
388
|
if (currentVersion === packageVersion) {
|
|
@@ -315,9 +446,16 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
315
446
|
}
|
|
316
447
|
|
|
317
448
|
if (resolution === 'backup') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
449
|
+
try {
|
|
450
|
+
const backupDir = await backupFiles(clientDir, modifiedNonOrphans);
|
|
451
|
+
backedUpFiles = modifiedNonOrphans;
|
|
452
|
+
const relativePath = backupDir.replace(clientDir + sep, '');
|
|
453
|
+
console.log(` Backed up ${modifiedNonOrphans.length} modified files to ${relativePath}`);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
logger.error(` Backup failed: ${err.message}`);
|
|
456
|
+
logger.error(' Update aborted — no files were overwritten.');
|
|
457
|
+
return { status: 'cancelled' };
|
|
458
|
+
}
|
|
321
459
|
}
|
|
322
460
|
}
|
|
323
461
|
|
|
@@ -345,16 +483,28 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
345
483
|
const srcPath = join(basePath, relativePath);
|
|
346
484
|
const destPath = join(clientDir, relativePath);
|
|
347
485
|
|
|
486
|
+
const srcChecksum = await calculateChecksum(srcPath);
|
|
348
487
|
await copyFileWithDirs(srcPath, destPath);
|
|
349
488
|
|
|
350
|
-
|
|
489
|
+
// Verify copy integrity (guards against cloud sync placeholder files)
|
|
490
|
+
let destChecksum = await calculateChecksum(destPath);
|
|
491
|
+
if (destChecksum !== srcChecksum) {
|
|
492
|
+
// Retry once — cloud sync may have interfered
|
|
493
|
+
await copyFileWithDirs(srcPath, destPath);
|
|
494
|
+
destChecksum = await calculateChecksum(destPath);
|
|
495
|
+
if (destChecksum !== srcChecksum) {
|
|
496
|
+
logger.error(` File write failed for ${relativePath} (possible cloud sync interference).`);
|
|
497
|
+
logger.error(' Update aborted — try running update again.');
|
|
498
|
+
return { status: 'cancelled' };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
351
501
|
|
|
352
502
|
// Preserve managedType from existing manifest or determine new
|
|
353
503
|
const existingEntry = manifest.managedFiles[relativePath];
|
|
354
504
|
const managedType = existingEntry?.managedType || getManagedType(relativePath);
|
|
355
505
|
|
|
356
506
|
updatedFiles[relativePath] = {
|
|
357
|
-
checksum,
|
|
507
|
+
checksum: srcChecksum,
|
|
358
508
|
version: packageVersion,
|
|
359
509
|
managedType
|
|
360
510
|
};
|
|
@@ -366,8 +516,15 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
366
516
|
// Back up modified orphans before removing
|
|
367
517
|
const modifiedOrphans = orphanedFiles.filter(f => mods.modified.includes(f));
|
|
368
518
|
if (modifiedOrphans.length > 0) {
|
|
369
|
-
|
|
370
|
-
|
|
519
|
+
try {
|
|
520
|
+
const backupDir = await backupFiles(clientDir, modifiedOrphans);
|
|
521
|
+
const relPath = backupDir.replace(clientDir + sep, '');
|
|
522
|
+
console.log(` Backed up ${modifiedOrphans.length} modified removed file${modifiedOrphans.length !== 1 ? 's' : ''} to ${relPath}`);
|
|
523
|
+
} catch (err) {
|
|
524
|
+
logger.error(` Backup of removed files failed: ${err.message}`);
|
|
525
|
+
logger.error(' Update aborted — no files were removed.');
|
|
526
|
+
return { status: 'cancelled' };
|
|
527
|
+
}
|
|
371
528
|
}
|
|
372
529
|
|
|
373
530
|
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
|
/**
|