ppcos 1.2.0 → 1.2.2
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 +80 -40
- package/lib/commands/init.js +38 -4
- 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,17 +4,23 @@
|
|
|
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';
|
|
17
|
-
import
|
|
18
|
+
import ora from 'ora';
|
|
19
|
+
import { getPackageVersion, getClientsDir } from '../utils/paths.js';
|
|
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']);
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Get config file path
|
|
@@ -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,72 @@ 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
|
+
// Use the hub-base/ extracted from the API zip — the published npm
|
|
231
|
+
// package only ships bin/ + lib/, so getPackageRoot()/hub-base doesn't
|
|
232
|
+
// exist for normal installs.
|
|
233
|
+
const hubDir = process.cwd();
|
|
234
|
+
if (!hubManifestExists(hubDir)) {
|
|
235
|
+
const hubBasePath = join(tempDir, 'hub-base');
|
|
236
|
+
await installHubSkills(hubBasePath);
|
|
237
|
+
}
|
|
201
238
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
239
|
+
// 5. Display summary
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log('Summary:');
|
|
242
|
+
console.log(` Initialized: ${results.initialized}`);
|
|
243
|
+
console.log(` Skipped: ${results.skipped}`);
|
|
244
|
+
console.log(` Failed: ${results.failed}`);
|
|
208
245
|
|
|
209
|
-
|
|
210
|
-
|
|
246
|
+
if (results.failed > 0) {
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
211
251
|
}
|
|
212
252
|
}
|
|
213
253
|
|
package/lib/commands/init.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Usage: ppcos init <client-name> [--skip-config]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync, rmSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
8
8
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
10
11
|
import { validateClientName } from '../utils/validation.js';
|
|
11
12
|
import { calculateChecksum } from '../utils/checksum.js';
|
|
12
13
|
import { createManifest, writeManifest, manifestExists, getManagedType, hubManifestExists, writeHubManifest } from '../utils/manifest.js';
|
|
@@ -142,9 +143,42 @@ async function createConfigTemplate() {
|
|
|
142
143
|
|
|
143
144
|
logger.success('Created main-config.json');
|
|
144
145
|
|
|
145
|
-
// Install hub skills
|
|
146
|
-
|
|
146
|
+
// Install hub skills. The published npm package ships only bin/ + lib/,
|
|
147
|
+
// so hub-base/ doesn't exist next to the installed package — we fetch
|
|
148
|
+
// the API zip into a temp dir and use its hub-base/ subdirectory. Falls
|
|
149
|
+
// back to the local hub-base/ when running from a repo checkout (dev mode).
|
|
150
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'ppcos-init-hub-'));
|
|
151
|
+
let hubBasePath;
|
|
152
|
+
const spinner = ora('Downloading hub skills...').start();
|
|
153
|
+
try {
|
|
154
|
+
await fetchSkills(tempDir);
|
|
155
|
+
spinner.succeed('Hub skills downloaded');
|
|
156
|
+
hubBasePath = join(tempDir, 'hub-base');
|
|
157
|
+
} catch (err) {
|
|
158
|
+
spinner.fail('Failed to download hub skills');
|
|
159
|
+
const localHubBase = getHubBasePath();
|
|
160
|
+
if (existsSync(localHubBase)) {
|
|
161
|
+
logger.warn('Using local hub-base/ (dev mode)');
|
|
162
|
+
hubBasePath = localHubBase;
|
|
163
|
+
} else {
|
|
164
|
+
logger.error(err.message);
|
|
165
|
+
logger.error('Hub skills not installed. Run: ppcos login, then ppcos init-all');
|
|
166
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
167
|
+
printNextSteps();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await installHubSkills(hubBasePath);
|
|
174
|
+
} finally {
|
|
175
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
printNextSteps();
|
|
179
|
+
}
|
|
147
180
|
|
|
181
|
+
function printNextSteps() {
|
|
148
182
|
console.log('');
|
|
149
183
|
console.log('Next steps:');
|
|
150
184
|
console.log(' 1. Edit main-config.json with your client names');
|
|
@@ -193,7 +227,7 @@ export default async function init(clientName, options = {}) {
|
|
|
193
227
|
// 3. Fetch skills from API (with local fallback for dev/testing)
|
|
194
228
|
const spinner = ora('Fetching skills from API...').start();
|
|
195
229
|
try {
|
|
196
|
-
await fetchSkills(clientDir);
|
|
230
|
+
await fetchSkills(clientDir, { excludeTopLevel: ['hub-base'] });
|
|
197
231
|
spinner.succeed('Skills downloaded');
|
|
198
232
|
} catch (error) {
|
|
199
233
|
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
|
}
|