ppcos 1.1.0 → 1.2.1
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/init-all.js +76 -39
- package/lib/commands/init.js +1 -1
- package/lib/commands/update.js +22 -1
- package/lib/utils/skills-fetcher.js +39 -4
- package/package.json +1 -1
package/lib/commands/init-all.js
CHANGED
|
@@ -4,18 +4,24 @@
|
|
|
4
4
|
* Usage: ppcos init-all [--force]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
8
8
|
import { readFile, mkdir, rename } from 'node:fs/promises';
|
|
9
9
|
import { join, dirname } from 'node:path';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
10
11
|
import { validateConfig } from '../utils/validation.js';
|
|
11
12
|
import { calculateChecksum } from '../utils/checksum.js';
|
|
12
13
|
import { createManifest, writeManifest, manifestExists, getManagedType, hubManifestExists } from '../utils/manifest.js';
|
|
13
|
-
import { getAllFiles } from '../utils/fs-helpers.js';
|
|
14
|
+
import { getAllFiles, copyFileWithDirs } from '../utils/fs-helpers.js';
|
|
14
15
|
import { fetchSkills } from '../utils/skills-fetcher.js';
|
|
15
16
|
import { installHubSkills } from './init.js';
|
|
16
17
|
import logger from '../utils/logger.js';
|
|
18
|
+
import ora from 'ora';
|
|
17
19
|
import { getPackageVersion, getPackageRoot, getClientsDir } from '../utils/paths.js';
|
|
18
20
|
|
|
21
|
+
// Top-level zip entries to skip when materializing a client folder.
|
|
22
|
+
// hub-base/ is for hub-level skills; tmp/ holds runtime artifacts.
|
|
23
|
+
const CLIENT_COPY_EXCLUDE_DIRS = new Set(['tmp', 'node_modules', 'hub-base']);
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
* Get config file path
|
|
21
27
|
* @returns {string}
|
|
@@ -77,13 +83,14 @@ async function backupClient(clientDir) {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/**
|
|
80
|
-
* Initialize a single client
|
|
86
|
+
* Initialize a single client by copying from a pre-extracted base template.
|
|
81
87
|
* @param {string} clientName
|
|
82
88
|
* @param {string} clientsDir
|
|
89
|
+
* @param {string} basePath - Directory holding the extracted skills zip
|
|
83
90
|
* @param {object} options
|
|
84
91
|
* @returns {Promise<{ status: 'initialized' | 'skipped' | 'failed', message?: string }>}
|
|
85
92
|
*/
|
|
86
|
-
async function initializeClient(clientName, clientsDir, options = {}) {
|
|
93
|
+
async function initializeClient(clientName, clientsDir, basePath, options = {}) {
|
|
87
94
|
const clientDir = join(clientsDir, clientName);
|
|
88
95
|
const version = getPackageVersion();
|
|
89
96
|
|
|
@@ -102,8 +109,15 @@ async function initializeClient(clientName, clientsDir, options = {}) {
|
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
try {
|
|
105
|
-
//
|
|
106
|
-
|
|
112
|
+
// Copy template files from the shared base to this client, skipping
|
|
113
|
+
// hub-base/ (hub-level skills, installed separately at the hub root).
|
|
114
|
+
const baseFiles = await getAllFiles(basePath, basePath, CLIENT_COPY_EXCLUDE_DIRS);
|
|
115
|
+
|
|
116
|
+
for (const relativePath of baseFiles) {
|
|
117
|
+
const srcPath = join(basePath, relativePath);
|
|
118
|
+
const destPath = join(clientDir, relativePath);
|
|
119
|
+
await copyFileWithDirs(srcPath, destPath);
|
|
120
|
+
}
|
|
107
121
|
|
|
108
122
|
// Calculate checksums for all files in client dir
|
|
109
123
|
const allFiles = await getAllFiles(clientDir);
|
|
@@ -168,46 +182,69 @@ export default async function initAll(options = {}) {
|
|
|
168
182
|
console.log(`Found ${enabledClients.length} client${enabledClients.length === 1 ? '' : 's'} in main-config.json`);
|
|
169
183
|
console.log('');
|
|
170
184
|
|
|
171
|
-
// 2.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
// 2. Download skills once into a shared temp dir, then copy locally
|
|
186
|
+
// per client. One API call serves N clients, avoiding rate limiting
|
|
187
|
+
// (API caps GET /api/skills at 10/hr per session).
|
|
188
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'ppcos-init-all-'));
|
|
189
|
+
const spinner = ora('Downloading skills from API...').start();
|
|
190
|
+
try {
|
|
191
|
+
await fetchSkills(tempDir);
|
|
192
|
+
spinner.succeed('Skills downloaded');
|
|
193
|
+
} catch (error) {
|
|
194
|
+
spinner.fail('Failed to download skills');
|
|
195
|
+
logger.error(error.message);
|
|
196
|
+
if (error.message.includes('Not authenticated') || error.message.includes('401') || error.message.includes('expired')) {
|
|
197
|
+
logger.info('Try: ppcos login');
|
|
198
|
+
}
|
|
199
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
// 3. Process each client
|
|
206
|
+
const results = {
|
|
207
|
+
initialized: 0,
|
|
208
|
+
skipped: 0,
|
|
209
|
+
failed: 0
|
|
210
|
+
};
|
|
177
211
|
|
|
178
|
-
|
|
179
|
-
|
|
212
|
+
for (const client of enabledClients) {
|
|
213
|
+
process.stdout.write(`Initializing ${client.name}... `);
|
|
180
214
|
|
|
181
|
-
|
|
215
|
+
const initResult = await initializeClient(client.name, clientsDir, tempDir, options);
|
|
182
216
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
if (initResult.status === 'initialized') {
|
|
218
|
+
logger.success('');
|
|
219
|
+
results.initialized++;
|
|
220
|
+
} else if (initResult.status === 'skipped') {
|
|
221
|
+
console.log(`Skipped (${initResult.message})`);
|
|
222
|
+
results.skipped++;
|
|
223
|
+
} else {
|
|
224
|
+
logger.error(initResult.message || 'Unknown error');
|
|
225
|
+
results.failed++;
|
|
226
|
+
}
|
|
192
227
|
}
|
|
193
|
-
}
|
|
194
228
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
229
|
+
// 4. Install hub skills if not already installed
|
|
230
|
+
const hubDir = process.cwd();
|
|
231
|
+
if (!hubManifestExists(hubDir)) {
|
|
232
|
+
const hubBasePath = join(getPackageRoot(), 'hub-base');
|
|
233
|
+
await installHubSkills(hubBasePath);
|
|
234
|
+
}
|
|
201
235
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
236
|
+
// 5. Display summary
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log('Summary:');
|
|
239
|
+
console.log(` Initialized: ${results.initialized}`);
|
|
240
|
+
console.log(` Skipped: ${results.skipped}`);
|
|
241
|
+
console.log(` Failed: ${results.failed}`);
|
|
208
242
|
|
|
209
|
-
|
|
210
|
-
|
|
243
|
+
if (results.failed > 0) {
|
|
244
|
+
process.exitCode = 1;
|
|
245
|
+
}
|
|
246
|
+
} finally {
|
|
247
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
211
248
|
}
|
|
212
249
|
}
|
|
213
250
|
|
package/lib/commands/init.js
CHANGED
|
@@ -193,7 +193,7 @@ export default async function init(clientName, options = {}) {
|
|
|
193
193
|
// 3. Fetch skills from API (with local fallback for dev/testing)
|
|
194
194
|
const spinner = ora('Fetching skills from API...').start();
|
|
195
195
|
try {
|
|
196
|
-
await fetchSkills(clientDir);
|
|
196
|
+
await fetchSkills(clientDir, { excludeTopLevel: ['hub-base'] });
|
|
197
197
|
spinner.succeed('Skills downloaded');
|
|
198
198
|
} catch (error) {
|
|
199
199
|
spinner.fail('Failed to fetch skills');
|
package/lib/commands/update.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Usage: ppcos update [--client <name>] [--dry-run]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync, unlinkSync } from 'node:fs';
|
|
7
|
+
import { existsSync, unlinkSync, statSync, chmodSync } from 'node:fs';
|
|
8
8
|
import { join, sep } from 'node:path';
|
|
9
9
|
import {
|
|
10
10
|
readManifest,
|
|
@@ -217,6 +217,27 @@ async function updateClient(clientName, basePathOrOptions = {}, options = {}) {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// Heal shell-hook exec bits across the full managed tree.
|
|
221
|
+
// Content-unchanged hooks are skipped by the checksum-driven copy loop above,
|
|
222
|
+
// so clients that updated before the skills-fetcher chmod fix kept 0644 hooks
|
|
223
|
+
// and Claude Code's stop hook fails with "Permission denied". This restores
|
|
224
|
+
// +x on any managed *.sh missing it. Idempotent; no-op on Windows.
|
|
225
|
+
if (process.platform !== 'win32') {
|
|
226
|
+
for (const relativePath of Object.keys(manifest.managedFiles)) {
|
|
227
|
+
if (!relativePath.endsWith('.sh')) continue;
|
|
228
|
+
const fullPath = join(clientDir, relativePath);
|
|
229
|
+
if (!existsSync(fullPath)) continue;
|
|
230
|
+
try {
|
|
231
|
+
const mode = statSync(fullPath).mode;
|
|
232
|
+
if ((mode & 0o111) !== 0o111) {
|
|
233
|
+
chmodSync(fullPath, mode | 0o755);
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
// Don't fail update over a chmod hiccup
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
220
241
|
// Update manifest
|
|
221
242
|
manifest.baseVersion = packageVersion;
|
|
222
243
|
manifest.lastUpdated = new Date().toISOString();
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, chmodSync } from 'fs';
|
|
2
2
|
import { Open } from 'unzipper';
|
|
3
|
-
import { join } from 'path';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
5
|
import { downloadSkills } from './api-client.js';
|
|
6
6
|
import { readAuth } from './auth.js';
|
|
7
|
+
import { getAllFiles } from './fs-helpers.js';
|
|
7
8
|
import logger from './logger.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Fetch skills from API and extract to target directory
|
|
11
12
|
* @param {string} targetDir - Directory to extract skills into
|
|
13
|
+
* @param {object} [options]
|
|
14
|
+
* @param {string[]} [options.excludeTopLevel] - Top-level zip entry names to skip
|
|
15
|
+
* (e.g. ['hub-base'] when extracting into a client folder — hub skills are
|
|
16
|
+
* handled separately via update-hub.js).
|
|
12
17
|
*/
|
|
13
|
-
export async function fetchSkills(targetDir) {
|
|
18
|
+
export async function fetchSkills(targetDir, options = {}) {
|
|
19
|
+
const { excludeTopLevel } = options;
|
|
14
20
|
const auth = readAuth();
|
|
15
21
|
|
|
16
22
|
if (!auth?.sessionToken) {
|
|
@@ -35,7 +41,36 @@ export async function fetchSkills(targetDir) {
|
|
|
35
41
|
writeFileSync(tempZip, buffer);
|
|
36
42
|
|
|
37
43
|
const directory = await Open.file(tempZip);
|
|
38
|
-
|
|
44
|
+
|
|
45
|
+
if (!excludeTopLevel || excludeTopLevel.length === 0) {
|
|
46
|
+
await directory.extract({ path: targetDir });
|
|
47
|
+
} else {
|
|
48
|
+
const excludeSet = new Set(excludeTopLevel);
|
|
49
|
+
for (const entry of directory.files) {
|
|
50
|
+
const top = entry.path.split('/')[0];
|
|
51
|
+
if (excludeSet.has(top)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const destPath = join(targetDir, entry.path);
|
|
55
|
+
if (entry.type === 'Directory') {
|
|
56
|
+
if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
|
|
57
|
+
} else {
|
|
58
|
+
const parent = dirname(destPath);
|
|
59
|
+
if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
|
|
60
|
+
const content = await entry.buffer();
|
|
61
|
+
writeFileSync(destPath, content);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// unzipper's extract() drops unix mode bits — restore +x on shell hooks
|
|
67
|
+
// so Claude Code can run them. No-op on Windows (chmod ignores mode bits).
|
|
68
|
+
const extracted = await getAllFiles(targetDir);
|
|
69
|
+
for (const rel of extracted) {
|
|
70
|
+
if (rel.endsWith('.sh')) {
|
|
71
|
+
try { chmodSync(join(targetDir, rel), 0o755); } catch {}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
39
74
|
} finally {
|
|
40
75
|
try { unlinkSync(tempZip); } catch {}
|
|
41
76
|
}
|