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.
- package/lib/commands/init-all.js +14 -37
- package/lib/commands/init.js +78 -36
- package/lib/commands/status.js +46 -60
- package/lib/commands/update-hub.js +205 -0
- package/lib/commands/update.js +25 -369
- package/lib/utils/conflicts.js +119 -0
- package/lib/utils/manifest.js +41 -1
- package/lib/utils/migrations.js +204 -0
- package/lib/utils/paths.js +88 -0
- package/package.json +1 -1
package/lib/commands/init-all.js
CHANGED
|
@@ -4,31 +4,17 @@
|
|
|
4
4
|
* Usage: ppcos init-all [--force]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
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.
|
|
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
|
-
|
|
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';
|
package/lib/commands/init.js
CHANGED
|
@@ -4,48 +4,17 @@
|
|
|
4
4
|
* Usage: ppcos init <client-name> [--skip-config]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
8
8
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
9
|
-
import { join
|
|
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 {
|
|
271
|
+
export { addToMainConfig, installHubSkills };
|
|
272
|
+
export { getPackageVersion, getBaseTemplatePath, getHubBasePath, getClientsDir } from '../utils/paths.js';
|
package/lib/commands/status.js
CHANGED
|
@@ -5,65 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
8
|
-
import { join
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { join } from 'node:path';
|
|
10
9
|
import {
|
|
11
10
|
readManifest,
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
+
}
|