ppcos 1.0.5 → 1.0.7

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.
@@ -4,31 +4,17 @@
4
4
  * Usage: ppcos init-all [--force]
5
5
  */
6
6
 
7
- import { readFileSync, existsSync } from 'node:fs';
7
+ import { existsSync } from 'node:fs';
8
8
  import { readFile, mkdir, rename } from 'node:fs/promises';
9
9
  import { join, dirname } from 'node:path';
10
- import { fileURLToPath } from 'node:url';
11
10
  import { validateConfig } from '../utils/validation.js';
12
11
  import { calculateChecksum } from '../utils/checksum.js';
13
- import { createManifest, writeManifest, manifestExists, getManagedType } from '../utils/manifest.js';
12
+ import { createManifest, writeManifest, manifestExists, getManagedType, hubManifestExists } from '../utils/manifest.js';
14
13
  import { getAllFiles } from '../utils/fs-helpers.js';
15
14
  import { fetchSkills } from '../utils/skills-fetcher.js';
15
+ import { installHubSkills } from './init.js';
16
16
  import logger from '../utils/logger.js';
17
-
18
- // Get package root directory
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
21
- const PACKAGE_ROOT = join(__dirname, '..', '..');
22
-
23
- /**
24
- * Get package version from package.json
25
- * @returns {string}
26
- */
27
- function getPackageVersion() {
28
- const pkgPath = join(PACKAGE_ROOT, 'package.json');
29
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
30
- return pkg.version;
31
- }
17
+ import { getPackageVersion, getPackageRoot, getClientsDir } from '../utils/paths.js';
32
18
 
33
19
  /**
34
20
  * Get config file path
@@ -38,16 +24,6 @@ function getConfigPath() {
38
24
  return join(process.cwd(), 'main-config.json');
39
25
  }
40
26
 
41
- /**
42
- * Get clients directory path from config or default
43
- * @param {object} config
44
- * @returns {string}
45
- */
46
- function getClientsDir(config) {
47
- const dir = config?.settings?.clientsDirectory || 'clients';
48
- return join(process.cwd(), dir);
49
- }
50
-
51
27
  /**
52
28
  * Load and validate main-config.json
53
29
  * @returns {{ config: object, errors: string[] } | null}
@@ -216,7 +192,14 @@ export default async function initAll(options = {}) {
216
192
  }
217
193
  }
218
194
 
219
- // 3. Display summary
195
+ // 3. Install hub skills if not already installed
196
+ const hubDir = process.cwd();
197
+ if (!hubManifestExists(hubDir)) {
198
+ const hubBasePath = join(getPackageRoot(), 'hub-base');
199
+ await installHubSkills(hubBasePath);
200
+ }
201
+
202
+ // 4. Display summary
220
203
  console.log('');
221
204
  console.log('Summary:');
222
205
  console.log(` Initialized: ${results.initialized}`);
@@ -229,11 +212,5 @@ export default async function initAll(options = {}) {
229
212
  }
230
213
 
231
214
  // Export helpers for testing
232
- export {
233
- getPackageVersion,
234
- getConfigPath,
235
- getClientsDir,
236
- loadConfig,
237
- initializeClient,
238
- backupClient
239
- };
215
+ export { getConfigPath, loadConfig, initializeClient, backupClient };
216
+ export { getPackageVersion, getClientsDir } from '../utils/paths.js';
@@ -4,48 +4,17 @@
4
4
  * Usage: ppcos init <client-name> [--skip-config]
5
5
  */
6
6
 
7
- import { readFileSync, existsSync, rmSync } from 'node:fs';
7
+ import { existsSync, rmSync } from 'node:fs';
8
8
  import { readFile, writeFile } from 'node:fs/promises';
9
- import { join, dirname } from 'node:path';
10
- import { fileURLToPath } from 'node:url';
9
+ import { join } from 'node:path';
11
10
  import { validateClientName } from '../utils/validation.js';
12
11
  import { calculateChecksum } from '../utils/checksum.js';
13
- import { createManifest, writeManifest, manifestExists, getManagedType } from '../utils/manifest.js';
12
+ import { createManifest, writeManifest, manifestExists, getManagedType, hubManifestExists, writeHubManifest } from '../utils/manifest.js';
14
13
  import { getAllFiles, copyFileWithDirs, ensureDir, fileExists } from '../utils/fs-helpers.js';
15
14
  import { fetchSkills } from '../utils/skills-fetcher.js';
16
15
  import logger from '../utils/logger.js';
17
16
  import ora from 'ora';
18
-
19
- // Get package root directory
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = dirname(__filename);
22
- const PACKAGE_ROOT = join(__dirname, '..', '..');
23
-
24
- /**
25
- * Get package version from package.json
26
- * @returns {string}
27
- */
28
- function getPackageVersion() {
29
- const pkgPath = join(PACKAGE_ROOT, 'package.json');
30
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
31
- return pkg.version;
32
- }
33
-
34
- /**
35
- * Get path to .claude-base template directory
36
- * @returns {string}
37
- */
38
- function getBaseTemplatePath() {
39
- return join(PACKAGE_ROOT, '.claude-base');
40
- }
41
-
42
- /**
43
- * Get clients directory path
44
- * @returns {string}
45
- */
46
- function getClientsDir() {
47
- return join(process.cwd(), 'clients');
48
- }
17
+ import { getPackageVersion, getBaseTemplatePath, getHubBasePath, getClientsDir } from '../utils/paths.js';
49
18
 
50
19
  /**
51
20
  * Add client to main-config.json
@@ -83,6 +52,73 @@ async function addToMainConfig(clientName) {
83
52
  logger.debug(`Added "${clientName}" to main-config.json`);
84
53
  }
85
54
 
55
+ /**
56
+ * Install hub-level skills to the hub root directory
57
+ * Hub skills live in hub-base/.claude/skills/ and get installed to {cwd}/.claude/skills/
58
+ * Only skill files are tracked in .managed-hub.json; my-brand/ and reports/ are user dirs.
59
+ * @param {string} hubBasePath - Path to hub-base template (local or extracted from zip)
60
+ */
61
+ async function installHubSkills(hubBasePath) {
62
+ const hubDir = process.cwd();
63
+
64
+ if (!existsSync(hubBasePath)) {
65
+ logger.debug('No hub-base template found, skipping hub skills');
66
+ return;
67
+ }
68
+
69
+ if (hubManifestExists(hubDir)) {
70
+ logger.debug('Hub skills already installed');
71
+ return;
72
+ }
73
+
74
+ const version = getPackageVersion();
75
+ const allHubFiles = await getAllFiles(hubBasePath);
76
+
77
+ // Separate skill files (tracked) from data directories (not tracked)
78
+ const skillFiles = allHubFiles.filter(f => f.startsWith('.claude/'));
79
+ const dataDirs = ['my-brand', 'reports'];
80
+
81
+ // Copy skill files and calculate checksums
82
+ const managedFiles = {};
83
+
84
+ for (const relativePath of skillFiles) {
85
+ const srcPath = join(hubBasePath, relativePath);
86
+ const destPath = join(hubDir, relativePath);
87
+ await copyFileWithDirs(srcPath, destPath);
88
+
89
+ // Verify copy integrity (guards against cloud sync placeholder files)
90
+ const srcChecksum = await calculateChecksum(srcPath);
91
+ let destChecksum = await calculateChecksum(destPath);
92
+ if (destChecksum !== srcChecksum) {
93
+ await copyFileWithDirs(srcPath, destPath);
94
+ destChecksum = await calculateChecksum(destPath);
95
+ if (destChecksum !== srcChecksum) {
96
+ logger.error(`File write failed for ${relativePath} (possible cloud sync interference).`);
97
+ logger.error('Try running the command again.');
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ }
102
+
103
+ managedFiles[relativePath] = {
104
+ checksum: srcChecksum,
105
+ version,
106
+ managedType: 'update'
107
+ };
108
+ }
109
+
110
+ // Create data directories (not tracked in manifest)
111
+ for (const dir of dataDirs) {
112
+ await ensureDir(join(hubDir, dir));
113
+ }
114
+
115
+ // Write hub manifest
116
+ const manifest = createManifest(version, managedFiles);
117
+ await writeHubManifest(hubDir, manifest);
118
+
119
+ logger.success(`Hub skills installed (${skillFiles.length} files)`);
120
+ }
121
+
86
122
  /**
87
123
  * Create main-config.json template
88
124
  */
@@ -105,10 +141,15 @@ async function createConfigTemplate() {
105
141
  await writeFile(configPath, JSON.stringify(template, null, 2) + '\n');
106
142
 
107
143
  logger.success('Created main-config.json');
144
+
145
+ // Install hub skills
146
+ await installHubSkills(getHubBasePath());
147
+
108
148
  console.log('');
109
149
  console.log('Next steps:');
110
150
  console.log(' 1. Edit main-config.json with your client names');
111
151
  console.log(' 2. Run: ppcos init-all');
152
+ console.log(' 3. Run: /my-brand to set up your agency branding');
112
153
  }
113
154
 
114
155
  /**
@@ -227,4 +268,5 @@ export default async function init(clientName, options = {}) {
227
268
  }
228
269
 
229
270
  // Export helpers for testing
230
- export { getPackageVersion, getBaseTemplatePath, getClientsDir, addToMainConfig };
271
+ export { addToMainConfig, installHubSkills };
272
+ export { getPackageVersion, getBaseTemplatePath, getHubBasePath, getClientsDir } from '../utils/paths.js';
@@ -5,65 +5,17 @@
5
5
  */
6
6
 
7
7
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
8
- import { join, dirname } from 'node:path';
9
- import { fileURLToPath } from 'node:url';
8
+ import { join } from 'node:path';
10
9
  import {
11
10
  readManifest,
12
- manifestExists,
13
- detectModifications
11
+ detectModifications,
12
+ readHubManifest,
13
+ hubManifestExists
14
14
  } from '../utils/manifest.js';
15
15
  import { readAuth } from '../utils/auth.js';
16
16
  import logger from '../utils/logger.js';
17
17
  import chalk from 'chalk';
18
-
19
- // Get package root directory
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = dirname(__filename);
22
- const PACKAGE_ROOT = join(__dirname, '..', '..');
23
-
24
- /**
25
- * Get package version from package.json
26
- * @returns {string}
27
- */
28
- function getPackageVersion() {
29
- const pkgPath = join(PACKAGE_ROOT, 'package.json');
30
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
31
- return pkg.version;
32
- }
33
-
34
- /**
35
- * Get clients directory path
36
- * @returns {string}
37
- */
38
- function getClientsDir() {
39
- return join(process.cwd(), 'clients');
40
- }
41
-
42
- /**
43
- * Find all client directories with .managed.json
44
- * @returns {string[]} Array of client names
45
- */
46
- function discoverClients() {
47
- const clientsDir = getClientsDir();
48
-
49
- if (!existsSync(clientsDir)) {
50
- return [];
51
- }
52
-
53
- const entries = readdirSync(clientsDir, { withFileTypes: true });
54
- const clients = [];
55
-
56
- for (const entry of entries) {
57
- if (entry.isDirectory()) {
58
- const clientDir = join(clientsDir, entry.name);
59
- if (manifestExists(clientDir)) {
60
- clients.push(entry.name);
61
- }
62
- }
63
- }
64
-
65
- return clients;
66
- }
18
+ import { getPackageVersion, getClientsDir, discoverClients } from '../utils/paths.js';
67
19
 
68
20
  /**
69
21
  * Find custom skills in a client directory
@@ -180,6 +132,45 @@ export default async function status(options = {}) {
180
132
  console.log('ppcos-hub/');
181
133
  console.log('');
182
134
 
135
+ // Hub skills status
136
+ const hubDir = process.cwd();
137
+ if (hubManifestExists(hubDir)) {
138
+ try {
139
+ const hubManifest = await readHubManifest(hubDir);
140
+ const hubMods = await detectModifications(hubDir, hubManifest);
141
+ const hubFileCount = Object.keys(hubManifest.managedFiles).length;
142
+
143
+ console.log(' Hub Skills');
144
+ if (hubManifest.baseVersion !== packageVersion) {
145
+ console.log(` Version: ${hubManifest.baseVersion} → ${packageVersion} available`);
146
+ } else {
147
+ console.log(` Version: ${hubManifest.baseVersion} (up to date)`);
148
+ }
149
+ console.log(` Managed: ${hubFileCount} files`);
150
+ console.log(` Modified: ${hubMods.modified.length} files`);
151
+ for (const file of hubMods.modified) {
152
+ console.log(` - ${file}`);
153
+ }
154
+
155
+ // Check branding status
156
+ const brandPath = join(hubDir, 'my-brand', 'brand.json');
157
+ if (existsSync(brandPath)) {
158
+ try {
159
+ const brand = JSON.parse(readFileSync(brandPath, 'utf8'));
160
+ console.log(` Branding: ${chalk.green('configured')} (${brand.company?.name || 'unknown'})`);
161
+ } catch {
162
+ console.log(` Branding: ${chalk.yellow('invalid brand.json')}`);
163
+ }
164
+ } else {
165
+ console.log(` Branding: ${chalk.yellow('not configured')} — run /my-brand`);
166
+ }
167
+ console.log('');
168
+ } catch (err) {
169
+ logger.error(`Failed to read hub status: ${err.message}`);
170
+ console.log('');
171
+ }
172
+ }
173
+
183
174
  // Process each client
184
175
  for (const clientName of clients) {
185
176
  try {
@@ -258,10 +249,5 @@ export default async function status(options = {}) {
258
249
  }
259
250
 
260
251
  // Export helpers for testing
261
- export {
262
- getPackageVersion,
263
- getClientsDir,
264
- discoverClients,
265
- findCustomSkills,
266
- getClientStatus
267
- };
252
+ export { findCustomSkills, getClientStatus };
253
+ export { getPackageVersion, getClientsDir, discoverClients } from '../utils/paths.js';
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Hub-level skill update command.
3
+ *
4
+ * Installs or updates hub skills (branding-generator, report-generator, etc.)
5
+ * tracked via .managed-hub.json at the project root.
6
+ */
7
+
8
+ import { existsSync, unlinkSync } from 'node:fs';
9
+ import { join, sep } from 'node:path';
10
+ import {
11
+ detectModifications,
12
+ addConflict,
13
+ readHubManifest,
14
+ writeHubManifest,
15
+ hubManifestExists,
16
+ createManifest
17
+ } from '../utils/manifest.js';
18
+ import { calculateChecksum } from '../utils/checksum.js';
19
+ import { getAllFiles, copyFileWithDirs, ensureDir } from '../utils/fs-helpers.js';
20
+ import logger from '../utils/logger.js';
21
+ import { getPackageVersion, getHubBasePath } from '../utils/paths.js';
22
+ import { promptConflictResolution, backupFiles } from '../utils/conflicts.js';
23
+
24
+ /**
25
+ * Update hub-level skills (branding-generator, report-generator, etc.)
26
+ * @param {string} basePath - Path to downloaded/local base template directory
27
+ * @param {object} options - Command options
28
+ * @param {boolean} options.dryRun - Dry run mode
29
+ */
30
+ export default async function updateHub(basePath, options = {}) {
31
+ const hubDir = process.cwd();
32
+
33
+ // Determine hub base path — either nested inside downloaded zip or local dev path
34
+ let hubBasePath = join(basePath, 'hub-base');
35
+ if (!existsSync(hubBasePath)) {
36
+ // Try local hub-base (dev mode)
37
+ hubBasePath = getHubBasePath();
38
+ if (!existsSync(hubBasePath)) {
39
+ return; // No hub-base available, skip silently
40
+ }
41
+ }
42
+
43
+ const hubSkillsDir = join(hubBasePath, '.claude');
44
+ if (!existsSync(hubSkillsDir)) {
45
+ return; // hub-base exists but has no .claude/ content
46
+ }
47
+
48
+ const packageVersion = getPackageVersion();
49
+
50
+ // Get all hub skill files (only .claude/ subtree is managed)
51
+ const allHubFiles = await getAllFiles(hubBasePath);
52
+ const skillFiles = allHubFiles.filter(f => f.startsWith('.claude/'));
53
+
54
+ if (skillFiles.length === 0) {
55
+ return;
56
+ }
57
+
58
+ // First-time installation
59
+ if (!hubManifestExists(hubDir)) {
60
+ if (options.dryRun) {
61
+ console.log('Hub Skills (first install):');
62
+ console.log(` Would install ${skillFiles.length} hub skill files`);
63
+ console.log('');
64
+ return;
65
+ }
66
+
67
+ const managedFiles = {};
68
+ for (const relativePath of skillFiles) {
69
+ const srcPath = join(hubBasePath, relativePath);
70
+ const destPath = join(hubDir, relativePath);
71
+ await copyFileWithDirs(srcPath, destPath);
72
+ const checksum = await calculateChecksum(destPath);
73
+ managedFiles[relativePath] = { checksum, version: packageVersion, managedType: 'update' };
74
+ }
75
+
76
+ // Create data directories
77
+ await ensureDir(join(hubDir, 'my-brand'));
78
+ await ensureDir(join(hubDir, 'reports'));
79
+
80
+ const manifest = createManifest(packageVersion, managedFiles);
81
+ await writeHubManifest(hubDir, manifest);
82
+ logger.success(`Hub skills installed (${skillFiles.length} files)`);
83
+ console.log('');
84
+ return;
85
+ }
86
+
87
+ // Update existing hub skills
88
+ const manifest = await readHubManifest(hubDir);
89
+ const currentVersion = manifest.baseVersion;
90
+
91
+ if (currentVersion === packageVersion) {
92
+ return; // Up to date
93
+ }
94
+
95
+ // Detect modifications
96
+ const mods = await detectModifications(hubDir, manifest, skillFiles);
97
+
98
+ if (options.dryRun) {
99
+ console.log(`Hub Skills (v${currentVersion} → v${packageVersion}):`);
100
+ console.log(` Update: ${mods.unchanged.length + mods.missing.length + mods.newInBase.length} files`);
101
+ if (mods.modified.length > 0) {
102
+ console.log(` Modified: ${mods.modified.length} files`);
103
+ for (const f of mods.modified) console.log(` - ${f}`);
104
+ }
105
+ if (mods.newInBase.length > 0) {
106
+ console.log(` New: ${mods.newInBase.length} files`);
107
+ for (const f of mods.newInBase) console.log(` - ${f}`);
108
+ }
109
+ console.log('');
110
+ return;
111
+ }
112
+
113
+ // Handle modified files
114
+ let resolution = null;
115
+ if (mods.modified.length > 0) {
116
+ console.log(`Updating Hub Skills (v${currentVersion} → v${packageVersion})`);
117
+ resolution = await promptConflictResolution(mods.modified, hubDir, hubBasePath);
118
+ if (resolution === 'cancel') {
119
+ console.log(' Hub update cancelled');
120
+ console.log('');
121
+ return;
122
+ }
123
+ if (resolution === 'backup') {
124
+ try {
125
+ const backupDir = await backupFiles(hubDir, mods.modified);
126
+ const relPath = backupDir.replace(hubDir + sep, '');
127
+ console.log(` Backed up ${mods.modified.length} modified hub files to ${relPath}`);
128
+ } catch (err) {
129
+ logger.error(` Hub backup failed: ${err.message}`);
130
+ logger.error(' Hub update aborted.');
131
+ return;
132
+ }
133
+ }
134
+ }
135
+
136
+ // Apply updates
137
+ const filesToUpdate = [];
138
+ const filesToSkip = [];
139
+
140
+ for (const file of skillFiles) {
141
+ if (mods.modified.includes(file) && resolution === 'skip') {
142
+ filesToSkip.push(file);
143
+ } else {
144
+ filesToUpdate.push(file);
145
+ }
146
+ }
147
+
148
+ const updatedFiles = {};
149
+ for (const relativePath of filesToUpdate) {
150
+ const srcPath = join(hubBasePath, relativePath);
151
+ const destPath = join(hubDir, relativePath);
152
+ const srcChecksum = await calculateChecksum(srcPath);
153
+ await copyFileWithDirs(srcPath, destPath);
154
+
155
+ // Verify copy integrity (guards against cloud sync placeholder files)
156
+ let destChecksum = await calculateChecksum(destPath);
157
+ if (destChecksum !== srcChecksum) {
158
+ await copyFileWithDirs(srcPath, destPath);
159
+ destChecksum = await calculateChecksum(destPath);
160
+ if (destChecksum !== srcChecksum) {
161
+ logger.error(` File write failed for ${relativePath} (possible cloud sync interference).`);
162
+ logger.error(' Hub update aborted — try running update again.');
163
+ return;
164
+ }
165
+ }
166
+
167
+ updatedFiles[relativePath] = { checksum: srcChecksum, version: packageVersion, managedType: 'update' };
168
+ }
169
+
170
+ // Detect and remove orphaned hub files
171
+ const hubBaseSet = new Set(skillFiles);
172
+ const orphaned = Object.keys(manifest.managedFiles).filter(f => !hubBaseSet.has(f));
173
+ let removedCount = 0;
174
+ for (const relativePath of orphaned) {
175
+ const fullPath = join(hubDir, relativePath);
176
+ if (existsSync(fullPath)) {
177
+ unlinkSync(fullPath);
178
+ removedCount++;
179
+ }
180
+ delete manifest.managedFiles[relativePath];
181
+ }
182
+
183
+ // Update manifest
184
+ manifest.baseVersion = packageVersion;
185
+ manifest.lastUpdated = new Date().toISOString();
186
+ manifest.conflicts = [];
187
+ for (const [path, info] of Object.entries(updatedFiles)) {
188
+ manifest.managedFiles[path] = info;
189
+ }
190
+ for (const file of filesToSkip) {
191
+ addConflict(manifest, file, 'User modified - skipped during update', packageVersion);
192
+ }
193
+ await writeHubManifest(hubDir, manifest);
194
+
195
+ console.log(` Updated ${filesToUpdate.length} hub skill files`);
196
+ if (removedCount > 0) console.log(` Removed ${removedCount} orphaned hub files`);
197
+ if (filesToSkip.length > 0) console.log(` Skipped ${filesToSkip.length} modified hub files`);
198
+ logger.success('Hub skills updated');
199
+ console.log('');
200
+ }