ppcos 1.0.6 → 1.0.8

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,205 @@
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
+ getManagedType
18
+ } from '../utils/manifest.js';
19
+ import { calculateChecksum } from '../utils/checksum.js';
20
+ import { getAllFiles, copyFileWithDirs, ensureDir } from '../utils/fs-helpers.js';
21
+ import logger from '../utils/logger.js';
22
+ import { getPackageVersion, getHubBasePath } from '../utils/paths.js';
23
+ import { promptConflictResolution, backupFiles } from '../utils/conflicts.js';
24
+
25
+ /**
26
+ * Update hub-level skills (branding-generator, report-generator, etc.)
27
+ * @param {string} basePath - Path to downloaded/local base template directory
28
+ * @param {object} options - Command options
29
+ * @param {boolean} options.dryRun - Dry run mode
30
+ */
31
+ export default async function updateHub(basePath, options = {}) {
32
+ const hubDir = process.cwd();
33
+
34
+ // Determine hub base path — either nested inside downloaded zip or local dev path
35
+ let hubBasePath = join(basePath, 'hub-base');
36
+ if (!existsSync(hubBasePath)) {
37
+ // Try local hub-base (dev mode)
38
+ hubBasePath = getHubBasePath();
39
+ if (!existsSync(hubBasePath)) {
40
+ return; // No hub-base available, skip silently
41
+ }
42
+ }
43
+
44
+ const hubSkillsDir = join(hubBasePath, '.claude');
45
+ if (!existsSync(hubSkillsDir)) {
46
+ return; // hub-base exists but has no .claude/ content
47
+ }
48
+
49
+ const packageVersion = getPackageVersion();
50
+
51
+ // Get all hub skill files (only .claude/ subtree is managed)
52
+ const allHubFiles = await getAllFiles(hubBasePath);
53
+ const skillFiles = allHubFiles.filter(f => f.startsWith('.claude/'));
54
+
55
+ if (skillFiles.length === 0) {
56
+ return;
57
+ }
58
+
59
+ // First-time installation
60
+ if (!hubManifestExists(hubDir)) {
61
+ if (options.dryRun) {
62
+ console.log('Hub Skills (first install):');
63
+ console.log(` Would install ${skillFiles.length} hub skill files`);
64
+ console.log('');
65
+ return;
66
+ }
67
+
68
+ const managedFiles = {};
69
+ for (const relativePath of skillFiles) {
70
+ const srcPath = join(hubBasePath, relativePath);
71
+ const destPath = join(hubDir, relativePath);
72
+ await copyFileWithDirs(srcPath, destPath);
73
+ const checksum = await calculateChecksum(destPath);
74
+ managedFiles[relativePath] = { checksum, version: packageVersion, managedType: getManagedType(relativePath) };
75
+ }
76
+
77
+ // Create data directories
78
+ await ensureDir(join(hubDir, 'my-brand'));
79
+ await ensureDir(join(hubDir, 'reports'));
80
+
81
+ const manifest = createManifest(packageVersion, managedFiles);
82
+ await writeHubManifest(hubDir, manifest);
83
+ logger.success(`Hub skills installed (${skillFiles.length} files)`);
84
+ console.log('');
85
+ return;
86
+ }
87
+
88
+ // Update existing hub skills
89
+ const manifest = await readHubManifest(hubDir);
90
+ const currentVersion = manifest.baseVersion;
91
+
92
+ if (currentVersion === packageVersion) {
93
+ return; // Up to date
94
+ }
95
+
96
+ // Detect modifications
97
+ const mods = await detectModifications(hubDir, manifest, skillFiles);
98
+
99
+ if (options.dryRun) {
100
+ console.log(`Hub Skills (v${currentVersion} → v${packageVersion}):`);
101
+ console.log(` Update: ${mods.unchanged.length + mods.missing.length + mods.newInBase.length} files`);
102
+ if (mods.modified.length > 0) {
103
+ console.log(` Modified: ${mods.modified.length} files`);
104
+ for (const f of mods.modified) console.log(` - ${f}`);
105
+ }
106
+ if (mods.newInBase.length > 0) {
107
+ console.log(` New: ${mods.newInBase.length} files`);
108
+ for (const f of mods.newInBase) console.log(` - ${f}`);
109
+ }
110
+ console.log('');
111
+ return;
112
+ }
113
+
114
+ // Handle modified files
115
+ let resolution = null;
116
+ if (mods.modified.length > 0) {
117
+ console.log(`Updating Hub Skills (v${currentVersion} → v${packageVersion})`);
118
+ resolution = await promptConflictResolution(mods.modified, hubDir, hubBasePath);
119
+ if (resolution === 'cancel') {
120
+ console.log(' Hub update cancelled');
121
+ console.log('');
122
+ return;
123
+ }
124
+ if (resolution === 'backup') {
125
+ try {
126
+ const backupDir = await backupFiles(hubDir, mods.modified);
127
+ const relPath = backupDir.replace(hubDir + sep, '');
128
+ console.log(` Backed up ${mods.modified.length} modified hub files to ${relPath}`);
129
+ } catch (err) {
130
+ logger.error(` Hub backup failed: ${err.message}`);
131
+ logger.error(' Hub update aborted.');
132
+ return;
133
+ }
134
+ }
135
+ }
136
+
137
+ // Apply updates
138
+ const filesToUpdate = [];
139
+ const filesToSkip = [];
140
+
141
+ for (const file of skillFiles) {
142
+ if (getManagedType(file) === 'config') {
143
+ // Config files are init-only — skip on update if already present
144
+ if (manifest.managedFiles[file]) continue;
145
+ }
146
+ if (mods.modified.includes(file) && resolution === 'skip') {
147
+ filesToSkip.push(file);
148
+ } else {
149
+ filesToUpdate.push(file);
150
+ }
151
+ }
152
+
153
+ const updatedFiles = {};
154
+ for (const relativePath of filesToUpdate) {
155
+ const srcPath = join(hubBasePath, relativePath);
156
+ const destPath = join(hubDir, relativePath);
157
+ const srcChecksum = await calculateChecksum(srcPath);
158
+ await copyFileWithDirs(srcPath, destPath);
159
+
160
+ // Verify copy integrity (guards against cloud sync placeholder files)
161
+ let destChecksum = await calculateChecksum(destPath);
162
+ if (destChecksum !== srcChecksum) {
163
+ await copyFileWithDirs(srcPath, destPath);
164
+ destChecksum = await calculateChecksum(destPath);
165
+ if (destChecksum !== srcChecksum) {
166
+ logger.error(` File write failed for ${relativePath} (possible cloud sync interference).`);
167
+ logger.error(' Hub update aborted — try running update again.');
168
+ return;
169
+ }
170
+ }
171
+
172
+ updatedFiles[relativePath] = { checksum: srcChecksum, version: packageVersion, managedType: getManagedType(relativePath) };
173
+ }
174
+
175
+ // Detect and remove orphaned hub files
176
+ const hubBaseSet = new Set(skillFiles);
177
+ const orphaned = Object.keys(manifest.managedFiles).filter(f => !hubBaseSet.has(f));
178
+ let removedCount = 0;
179
+ for (const relativePath of orphaned) {
180
+ const fullPath = join(hubDir, relativePath);
181
+ if (existsSync(fullPath)) {
182
+ unlinkSync(fullPath);
183
+ removedCount++;
184
+ }
185
+ delete manifest.managedFiles[relativePath];
186
+ }
187
+
188
+ // Update manifest
189
+ manifest.baseVersion = packageVersion;
190
+ manifest.lastUpdated = new Date().toISOString();
191
+ manifest.conflicts = [];
192
+ for (const [path, info] of Object.entries(updatedFiles)) {
193
+ manifest.managedFiles[path] = info;
194
+ }
195
+ for (const file of filesToSkip) {
196
+ addConflict(manifest, file, 'User modified - skipped during update', packageVersion);
197
+ }
198
+ await writeHubManifest(hubDir, manifest);
199
+
200
+ console.log(` Updated ${filesToUpdate.length} hub skill files`);
201
+ if (removedCount > 0) console.log(` Removed ${removedCount} orphaned hub files`);
202
+ if (filesToSkip.length > 0) console.log(` Skipped ${filesToSkip.length} modified hub files`);
203
+ logger.success('Hub skills updated');
204
+ console.log('');
205
+ }