ppcos 1.3.0 → 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 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
- resolution = await promptConflictResolution(mods.modified, hubDir, hubBasePath);
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('');
@@ -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
- resolution = await promptConflictResolution(modifiedNonOrphans, clientDir, basePath);
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
  }
@@ -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
- throw new Error(error.error || 'Failed to download skills');
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;
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ppcos",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI tool to manage Google Ads AI workflow skills and agents for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {