ppcos 1.3.1 → 1.4.0
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/bin/ppcos.js +2 -0
- package/lib/commands/update-hub.js +8 -1
- package/lib/commands/update.js +13 -1
- package/lib/utils/api-client.js +5 -1
- package/lib/utils/migrations.js +64 -12
- package/package.json +1 -1
package/bin/ppcos.js
CHANGED
|
@@ -81,6 +81,8 @@ program
|
|
|
81
81
|
.description('Update base skills in all clients while preserving custom work')
|
|
82
82
|
.option('--client <name>', 'Update only this client')
|
|
83
83
|
.option('--dry-run', 'Show changes without applying')
|
|
84
|
+
.option('--skip-modified', 'Non-interactive: keep locally modified managed files')
|
|
85
|
+
.option('--keep-local', 'Alias for --skip-modified')
|
|
84
86
|
.action(gated(update));
|
|
85
87
|
|
|
86
88
|
// status
|
|
@@ -27,6 +27,8 @@ import { promptConflictResolution, backupFiles } from '../utils/conflicts.js';
|
|
|
27
27
|
* @param {string} basePath - Path to downloaded/local base template directory
|
|
28
28
|
* @param {object} options - Command options
|
|
29
29
|
* @param {boolean} options.dryRun - Dry run mode
|
|
30
|
+
* @param {boolean} options.skipModified - Keep modified managed files without prompting
|
|
31
|
+
* @param {boolean} options.keepLocal - Alias for skipModified
|
|
30
32
|
*/
|
|
31
33
|
export default async function updateHub(basePath, options = {}) {
|
|
32
34
|
const hubDir = process.cwd();
|
|
@@ -113,9 +115,14 @@ export default async function updateHub(basePath, options = {}) {
|
|
|
113
115
|
|
|
114
116
|
// Handle modified files
|
|
115
117
|
let resolution = null;
|
|
118
|
+
const skipModified = options.skipModified || options.keepLocal;
|
|
116
119
|
if (mods.modified.length > 0) {
|
|
117
120
|
console.log(`Updating Hub Skills (v${currentVersion} → v${packageVersion})`);
|
|
118
|
-
|
|
121
|
+
if (skipModified) {
|
|
122
|
+
resolution = 'skip';
|
|
123
|
+
} else {
|
|
124
|
+
resolution = await promptConflictResolution(mods.modified, hubDir, hubBasePath);
|
|
125
|
+
}
|
|
119
126
|
if (resolution === 'cancel') {
|
|
120
127
|
console.log(' Hub update cancelled');
|
|
121
128
|
console.log('');
|
package/lib/commands/update.js
CHANGED
|
@@ -41,6 +41,8 @@ const CLIENT_TEMPLATE_EXCLUDE_DIRS = new Set(['tmp', 'hub-base']);
|
|
|
41
41
|
* @param {string|object} basePathOrOptions - Path to template files (temp dir or local), or options object for backward compat
|
|
42
42
|
* @param {object} options - Command options
|
|
43
43
|
* @param {boolean} options.dryRun - Dry run mode
|
|
44
|
+
* @param {boolean} options.skipModified - Keep modified managed files without prompting
|
|
45
|
+
* @param {boolean} options.keepLocal - Alias for skipModified
|
|
44
46
|
* @returns {Promise<{status: 'updated'|'up-to-date'|'skipped'|'cancelled', details?: object}>}
|
|
45
47
|
*/
|
|
46
48
|
async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
@@ -116,10 +118,15 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
116
118
|
// Handle modified files (excluding orphans, which are handled separately)
|
|
117
119
|
let resolution = null;
|
|
118
120
|
let backedUpFiles = [];
|
|
121
|
+
const skipModified = options.skipModified || options.keepLocal;
|
|
119
122
|
|
|
120
123
|
if (modifiedNonOrphans.length > 0) {
|
|
121
124
|
console.log(`Updating ${clientName} (v${currentVersion} → v${packageVersion})`);
|
|
122
|
-
|
|
125
|
+
if (skipModified) {
|
|
126
|
+
resolution = 'skip';
|
|
127
|
+
} else {
|
|
128
|
+
resolution = await promptConflictResolution(modifiedNonOrphans, clientDir, basePath);
|
|
129
|
+
}
|
|
123
130
|
|
|
124
131
|
if (resolution === 'cancel') {
|
|
125
132
|
return { status: 'cancelled' };
|
|
@@ -273,6 +280,8 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
273
280
|
* @param {object} options - Command options
|
|
274
281
|
* @param {string} [options.client] - Update only this client
|
|
275
282
|
* @param {boolean} [options.dryRun] - Show changes without applying
|
|
283
|
+
* @param {boolean} [options.skipModified] - Keep modified managed files without prompting
|
|
284
|
+
* @param {boolean} [options.keepLocal] - Alias for skipModified
|
|
276
285
|
*/
|
|
277
286
|
export default async function update(options = {}) {
|
|
278
287
|
const packageVersion = getPackageVersion();
|
|
@@ -303,6 +312,9 @@ export default async function update(options = {}) {
|
|
|
303
312
|
|
|
304
313
|
if (error.message.includes('Not authenticated') || error.message.includes('401') || error.message.includes('expired')) {
|
|
305
314
|
logger.info('Try: ppcos login');
|
|
315
|
+
} else if (error.status === 429 && error.retryAfter) {
|
|
316
|
+
const minutes = Math.ceil(error.retryAfter / 60);
|
|
317
|
+
logger.info(`Try again in ${minutes} minute${minutes !== 1 ? 's' : ''}.`);
|
|
306
318
|
} else {
|
|
307
319
|
logger.info('Check your internet connection and try again.');
|
|
308
320
|
}
|
package/lib/utils/api-client.js
CHANGED
|
@@ -108,7 +108,11 @@ export async function downloadSkills(sessionToken) {
|
|
|
108
108
|
|
|
109
109
|
if (!response.ok) {
|
|
110
110
|
const error = await response.json().catch(() => ({ error: 'Download failed' }));
|
|
111
|
-
|
|
111
|
+
const message = error.error || 'Failed to download skills';
|
|
112
|
+
const downloadError = new Error(message);
|
|
113
|
+
downloadError.status = response.status;
|
|
114
|
+
downloadError.retryAfter = error.retryAfter;
|
|
115
|
+
throw downloadError;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
return response;
|
package/lib/utils/migrations.js
CHANGED
|
@@ -87,18 +87,6 @@ export function migrateSettingsHooks(clientDir) {
|
|
|
87
87
|
}
|
|
88
88
|
]
|
|
89
89
|
}
|
|
90
|
-
],
|
|
91
|
-
PostCompact: [
|
|
92
|
-
{
|
|
93
|
-
hooks: [
|
|
94
|
-
{
|
|
95
|
-
type: 'command',
|
|
96
|
-
command: '.claude/hooks/post-compact-reminder.sh',
|
|
97
|
-
timeout: 30,
|
|
98
|
-
statusMessage: 'Restoring context after compaction...'
|
|
99
|
-
}
|
|
100
|
-
]
|
|
101
|
-
}
|
|
102
90
|
]
|
|
103
91
|
};
|
|
104
92
|
|
|
@@ -123,6 +111,69 @@ export function migrateSettingsHooks(clientDir) {
|
|
|
123
111
|
}
|
|
124
112
|
}
|
|
125
113
|
|
|
114
|
+
/**
|
|
115
|
+
* One-time migration: repair the post-compaction context hook.
|
|
116
|
+
*
|
|
117
|
+
* Earlier versions shipped a `PostCompact` hook event. That key is not a valid
|
|
118
|
+
* hook event in Claude Code's settings schema, so the whole settings.local.json
|
|
119
|
+
* file is rejected — silently disabling permissions and every other hook. It
|
|
120
|
+
* also never worked for its purpose: PostCompact does not inject stdout back
|
|
121
|
+
* into the session. The correct mechanism is a SessionStart hook with
|
|
122
|
+
* matcher "compact", whose stdout is added to context after compaction.
|
|
123
|
+
*
|
|
124
|
+
* This removes any `PostCompact` event and ensures the SessionStart "compact"
|
|
125
|
+
* group exists, pointing at the same post-compact-reminder.sh script.
|
|
126
|
+
* settings.local.json is config-only (never overwritten by update), so existing
|
|
127
|
+
* clients can only be repaired here.
|
|
128
|
+
*/
|
|
129
|
+
export function migrateCompactSessionHook(clientDir) {
|
|
130
|
+
const settingsPath = join(clientDir, '.claude', 'settings.local.json');
|
|
131
|
+
if (!existsSync(settingsPath)) return;
|
|
132
|
+
|
|
133
|
+
const COMPACT_SCRIPT = '.claude/hooks/post-compact-reminder.sh';
|
|
134
|
+
const compactGroup = {
|
|
135
|
+
matcher: 'compact',
|
|
136
|
+
hooks: [
|
|
137
|
+
{
|
|
138
|
+
type: 'command',
|
|
139
|
+
command: COMPACT_SCRIPT,
|
|
140
|
+
timeout: 30,
|
|
141
|
+
statusMessage: 'Restoring context after compaction...'
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
148
|
+
if (!settings.hooks) settings.hooks = {};
|
|
149
|
+
|
|
150
|
+
let changed = false;
|
|
151
|
+
|
|
152
|
+
// Remove the invalid PostCompact event if present.
|
|
153
|
+
if (settings.hooks.PostCompact) {
|
|
154
|
+
delete settings.hooks.PostCompact;
|
|
155
|
+
changed = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Ensure a SessionStart "compact" group running the reminder script exists.
|
|
159
|
+
const sessionStart = settings.hooks.SessionStart || [];
|
|
160
|
+
const hasCompact = sessionStart.some(group =>
|
|
161
|
+
(group.hooks || []).some(h => h.command === COMPACT_SCRIPT)
|
|
162
|
+
);
|
|
163
|
+
if (!hasCompact) {
|
|
164
|
+
settings.hooks.SessionStart = [...sessionStart, compactGroup];
|
|
165
|
+
changed = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!changed) return;
|
|
169
|
+
|
|
170
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
171
|
+
logger.info(' Repaired post-compaction hook (PostCompact -> SessionStart "compact")');
|
|
172
|
+
} catch {
|
|
173
|
+
// Don't fail update over migration
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
126
177
|
/**
|
|
127
178
|
* One-time migration: normalize backslash manifest keys to forward slashes.
|
|
128
179
|
* On Windows, getAllFiles() returned backslash paths which were stored as manifest keys.
|
|
@@ -198,6 +249,7 @@ export async function runAllMigrations(clientDir, manifest) {
|
|
|
198
249
|
await ensureMemoryFolder(clientDir);
|
|
199
250
|
migrateSettingsDenyRules(clientDir);
|
|
200
251
|
migrateSettingsHooks(clientDir);
|
|
252
|
+
migrateCompactSessionHook(clientDir);
|
|
201
253
|
migrateManifestKeys(manifest);
|
|
202
254
|
migrateConfigManagedTypes(manifest);
|
|
203
255
|
migrateTmpDataFiles(clientDir, manifest);
|