ppcos 0.3.0
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/LICENSE +9 -0
- package/README.md +246 -0
- package/bin/ppcos.js +91 -0
- package/lib/commands/init-all.js +247 -0
- package/lib/commands/init.js +229 -0
- package/lib/commands/login.js +85 -0
- package/lib/commands/logout.js +17 -0
- package/lib/commands/status.js +289 -0
- package/lib/commands/update.js +480 -0
- package/lib/commands/whoami.js +42 -0
- package/lib/utils/api-client.js +119 -0
- package/lib/utils/auth.js +117 -0
- package/lib/utils/checksum.js +61 -0
- package/lib/utils/fs-helpers.js +172 -0
- package/lib/utils/logger.js +51 -0
- package/lib/utils/manifest.js +212 -0
- package/lib/utils/skills-fetcher.js +50 -0
- package/lib/utils/validation.js +176 -0
- package/package.json +36 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init command - Create a new client workspace
|
|
3
|
+
*
|
|
4
|
+
* Usage: ppcos init <client-name> [--skip-config]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { validateClientName } from '../utils/validation.js';
|
|
12
|
+
import { calculateChecksum } from '../utils/checksum.js';
|
|
13
|
+
import { createManifest, writeManifest, manifestExists, getManagedType } from '../utils/manifest.js';
|
|
14
|
+
import { getAllFiles, copyFileWithDirs, ensureDir, fileExists } from '../utils/fs-helpers.js';
|
|
15
|
+
import { fetchSkills } from '../utils/skills-fetcher.js';
|
|
16
|
+
import { requireAuth } from '../utils/auth.js';
|
|
17
|
+
import logger from '../utils/logger.js';
|
|
18
|
+
import ora from 'ora';
|
|
19
|
+
|
|
20
|
+
// Get package root directory
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get package version from package.json
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function getPackageVersion() {
|
|
30
|
+
const pkgPath = join(PACKAGE_ROOT, 'package.json');
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
32
|
+
return pkg.version;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get path to .claude-base template directory
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function getBaseTemplatePath() {
|
|
40
|
+
return join(PACKAGE_ROOT, '.claude-base');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get clients directory path
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function getClientsDir() {
|
|
48
|
+
return join(process.cwd(), 'clients');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Add client to main-config.json
|
|
53
|
+
* @param {string} clientName
|
|
54
|
+
*/
|
|
55
|
+
async function addToMainConfig(clientName) {
|
|
56
|
+
const configPath = join(process.cwd(), 'main-config.json');
|
|
57
|
+
|
|
58
|
+
let config;
|
|
59
|
+
if (existsSync(configPath)) {
|
|
60
|
+
const content = await readFile(configPath, 'utf8');
|
|
61
|
+
config = JSON.parse(content);
|
|
62
|
+
|
|
63
|
+
// Check if client already exists
|
|
64
|
+
const exists = config.clients.some(c => c.name === clientName);
|
|
65
|
+
if (exists) {
|
|
66
|
+
logger.debug(`Client "${clientName}" already in main-config.json`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// Create new config
|
|
71
|
+
config = {
|
|
72
|
+
version: '1.0',
|
|
73
|
+
clients: []
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add the new client
|
|
78
|
+
config.clients.push({
|
|
79
|
+
name: clientName,
|
|
80
|
+
enabled: true
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
84
|
+
logger.debug(`Added "${clientName}" to main-config.json`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create main-config.json template
|
|
89
|
+
*/
|
|
90
|
+
async function createConfigTemplate() {
|
|
91
|
+
const configPath = join(process.cwd(), 'main-config.json');
|
|
92
|
+
|
|
93
|
+
if (existsSync(configPath)) {
|
|
94
|
+
logger.error('main-config.json already exists.');
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const template = {
|
|
100
|
+
version: '1.0',
|
|
101
|
+
clients: [
|
|
102
|
+
{ name: 'client-example', enabled: true }
|
|
103
|
+
]
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await writeFile(configPath, JSON.stringify(template, null, 2) + '\n');
|
|
107
|
+
|
|
108
|
+
logger.success('Created main-config.json');
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log('Next steps:');
|
|
111
|
+
console.log(' 1. Edit main-config.json with your client names');
|
|
112
|
+
console.log(' 2. Run: ppcos init-all');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Main init command handler
|
|
117
|
+
* @param {string} [clientName] - Name of the client (optional)
|
|
118
|
+
* @param {object} options - Command options
|
|
119
|
+
* @param {boolean} options.skipConfig - Skip adding to main-config.json
|
|
120
|
+
*/
|
|
121
|
+
export default async function init(clientName, options = {}) {
|
|
122
|
+
// No client name = create config template
|
|
123
|
+
if (!clientName) {
|
|
124
|
+
await createConfigTemplate();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 1. Validate client name
|
|
129
|
+
const validation = validateClientName(clientName);
|
|
130
|
+
if (!validation.valid) {
|
|
131
|
+
logger.error(`Invalid client name "${clientName}". ${validation.error}`);
|
|
132
|
+
logger.info('Use lowercase letters, numbers, and hyphens only. Must start with a letter.');
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. Check for existing folder
|
|
138
|
+
const clientsDir = getClientsDir();
|
|
139
|
+
const clientDir = join(clientsDir, clientName);
|
|
140
|
+
|
|
141
|
+
if (existsSync(clientDir)) {
|
|
142
|
+
if (manifestExists(clientDir)) {
|
|
143
|
+
logger.error(`Folder "clients/${clientName}" already exists with a managed workspace.`);
|
|
144
|
+
logger.info("Use 'ppcos update' to update existing clients.");
|
|
145
|
+
} else {
|
|
146
|
+
logger.error(`Folder "clients/${clientName}" already exists.`);
|
|
147
|
+
logger.info('Remove it manually or choose a different name.');
|
|
148
|
+
}
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 3. Fetch skills from API (with local fallback for dev/testing)
|
|
154
|
+
const spinner = ora('Fetching skills from API...').start();
|
|
155
|
+
let usedLocalFallback = false;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await fetchSkills(clientDir);
|
|
159
|
+
spinner.succeed('Skills downloaded');
|
|
160
|
+
} catch (error) {
|
|
161
|
+
spinner.fail('Failed to fetch skills');
|
|
162
|
+
|
|
163
|
+
// Fallback to local .claude-base for development/testing
|
|
164
|
+
const basePath = getBaseTemplatePath();
|
|
165
|
+
if (existsSync(basePath)) {
|
|
166
|
+
logger.warn('Using local .claude-base (dev/testing mode)');
|
|
167
|
+
const baseFiles = await getAllFiles(basePath);
|
|
168
|
+
|
|
169
|
+
for (const relativePath of baseFiles) {
|
|
170
|
+
const srcPath = join(basePath, relativePath);
|
|
171
|
+
const destPath = join(clientDir, relativePath);
|
|
172
|
+
await copyFileWithDirs(srcPath, destPath);
|
|
173
|
+
}
|
|
174
|
+
usedLocalFallback = true;
|
|
175
|
+
} else {
|
|
176
|
+
logger.error(error.message);
|
|
177
|
+
logger.error('No local fallback available. Run: ppcos login');
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 4. Calculate checksums for all downloaded files
|
|
184
|
+
const version = getPackageVersion();
|
|
185
|
+
const baseFiles = await getAllFiles(clientDir);
|
|
186
|
+
const managedFiles = {};
|
|
187
|
+
|
|
188
|
+
const checksumSpinner = ora(`Calculating checksums for ${baseFiles.length} files...`).start();
|
|
189
|
+
|
|
190
|
+
for (const relativePath of baseFiles) {
|
|
191
|
+
const filePath = join(clientDir, relativePath);
|
|
192
|
+
const checksum = await calculateChecksum(filePath);
|
|
193
|
+
const managedType = getManagedType(relativePath);
|
|
194
|
+
managedFiles[relativePath] = {
|
|
195
|
+
checksum,
|
|
196
|
+
version,
|
|
197
|
+
managedType
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
checksumSpinner.succeed('Checksums calculated');
|
|
202
|
+
|
|
203
|
+
// 5. Generate and write .managed.json
|
|
204
|
+
const manifest = createManifest(version, managedFiles);
|
|
205
|
+
await writeManifest(clientDir, manifest);
|
|
206
|
+
|
|
207
|
+
// 6. Update main-config.json (unless --skip-config)
|
|
208
|
+
if (!options.skipConfig) {
|
|
209
|
+
try {
|
|
210
|
+
await addToMainConfig(clientName);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
logger.warn(`Could not update main-config.json: ${err.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 7. Display success message
|
|
217
|
+
console.log('');
|
|
218
|
+
logger.success(`Client "${clientName}" initialized (v${version})`);
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(`Location: clients/${clientName}/`);
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log('Next steps:');
|
|
223
|
+
console.log(` 1. cd clients/${clientName}`);
|
|
224
|
+
console.log(' 2. Customize CLAUDE.md for this client');
|
|
225
|
+
console.log(' 3. Run /ppcos <website> to gather brand context');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Export helpers for testing
|
|
229
|
+
export { getPackageVersion, getBaseTemplatePath, getClientsDir, addToMainConfig };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import logger from '../utils/logger.js';
|
|
3
|
+
import { writeAuth } from '../utils/auth.js';
|
|
4
|
+
import { sendVerificationCode, verifyCode } from '../utils/api-client.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interactive login flow with email + OTP
|
|
9
|
+
*/
|
|
10
|
+
export async function login() {
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Step 1: Get email
|
|
18
|
+
const email = await new Promise((resolve) => {
|
|
19
|
+
rl.question('Email: ', resolve);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!email || !email.includes('@')) {
|
|
23
|
+
logger.error('Invalid email address');
|
|
24
|
+
rl.close();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Step 2: Send code
|
|
29
|
+
// Close readline before spinner to avoid terminal conflicts
|
|
30
|
+
rl.close();
|
|
31
|
+
|
|
32
|
+
const spinner = ora('Sending verification code...').start();
|
|
33
|
+
|
|
34
|
+
let sendResult;
|
|
35
|
+
try {
|
|
36
|
+
sendResult = await sendVerificationCode(email.trim());
|
|
37
|
+
spinner.succeed('Code sent to your email');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
spinner.fail('Failed to send code');
|
|
40
|
+
logger.error(error.message);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 3: Get code
|
|
45
|
+
// Recreate readline after spinner
|
|
46
|
+
const rl2 = readline.createInterface({
|
|
47
|
+
input: process.stdin,
|
|
48
|
+
output: process.stdout
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const code = await new Promise((resolve) => {
|
|
52
|
+
rl2.question('Enter verification code: ', resolve);
|
|
53
|
+
});
|
|
54
|
+
rl2.close();
|
|
55
|
+
|
|
56
|
+
if (!code || code.length !== 6) {
|
|
57
|
+
logger.error('Invalid code format');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 4: Verify code
|
|
62
|
+
const verifySpinner = ora('Verifying...').start();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = await verifyCode(email.trim(), code.trim(), sendResult.token);
|
|
66
|
+
verifySpinner.succeed('Verified');
|
|
67
|
+
|
|
68
|
+
// Step 5: Store session
|
|
69
|
+
writeAuth({
|
|
70
|
+
email: result.email,
|
|
71
|
+
sessionToken: result.sessionToken,
|
|
72
|
+
expiresAt: result.expiresAt
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
logger.success(`✓ Logged in as ${result.email}`);
|
|
76
|
+
logger.info(`Session expires: ${new Date(result.expiresAt).toLocaleString()}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
verifySpinner.fail('Verification failed');
|
|
79
|
+
logger.error(error.message);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error(`Login failed: ${error.message}`);
|
|
83
|
+
rl.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import logger from '../utils/logger.js';
|
|
2
|
+
import { clearAuth, readAuth } from '../utils/auth.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Clear authentication session
|
|
6
|
+
*/
|
|
7
|
+
export async function logout() {
|
|
8
|
+
const auth = readAuth();
|
|
9
|
+
|
|
10
|
+
if (!auth) {
|
|
11
|
+
logger.info('Not logged in');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
clearAuth();
|
|
16
|
+
logger.success(`✓ Logged out ${auth.email}`);
|
|
17
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status command - Show version and modification status
|
|
3
|
+
*
|
|
4
|
+
* Usage: ppcos status [--client <name>]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import {
|
|
11
|
+
readManifest,
|
|
12
|
+
manifestExists,
|
|
13
|
+
detectModifications
|
|
14
|
+
} from '../utils/manifest.js';
|
|
15
|
+
import { getAllFiles } from '../utils/fs-helpers.js';
|
|
16
|
+
import { readAuth } from '../utils/auth.js';
|
|
17
|
+
import logger from '../utils/logger.js';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
|
|
20
|
+
// Get package root directory
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get package version from package.json
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function getPackageVersion() {
|
|
30
|
+
const pkgPath = join(PACKAGE_ROOT, 'package.json');
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
32
|
+
return pkg.version;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get path to .claude-base template directory
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function getBaseTemplatePath() {
|
|
40
|
+
return join(PACKAGE_ROOT, '.claude-base');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get clients directory path
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function getClientsDir() {
|
|
48
|
+
return join(process.cwd(), 'clients');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find all client directories with .managed.json
|
|
53
|
+
* @returns {string[]} Array of client names
|
|
54
|
+
*/
|
|
55
|
+
function discoverClients() {
|
|
56
|
+
const clientsDir = getClientsDir();
|
|
57
|
+
|
|
58
|
+
if (!existsSync(clientsDir)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const entries = readdirSync(clientsDir, { withFileTypes: true });
|
|
63
|
+
const clients = [];
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
const clientDir = join(clientsDir, entry.name);
|
|
68
|
+
if (manifestExists(clientDir)) {
|
|
69
|
+
clients.push(entry.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return clients;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find custom skills in a client directory
|
|
79
|
+
* Custom skills are skill directories not tracked in managedFiles
|
|
80
|
+
* @param {string} clientDir - Path to client directory
|
|
81
|
+
* @param {object} manifest - The manifest object
|
|
82
|
+
* @returns {string[]} Array of custom skill names
|
|
83
|
+
*/
|
|
84
|
+
function findCustomSkills(clientDir, manifest) {
|
|
85
|
+
const skillsDir = join(clientDir, '.claude', 'skills');
|
|
86
|
+
|
|
87
|
+
if (!existsSync(skillsDir)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
92
|
+
const customSkills = [];
|
|
93
|
+
|
|
94
|
+
// Get managed skill directories from manifest
|
|
95
|
+
const managedSkillDirs = new Set();
|
|
96
|
+
for (const path of Object.keys(manifest.managedFiles)) {
|
|
97
|
+
if (path.startsWith('.claude/skills/')) {
|
|
98
|
+
const parts = path.split('/');
|
|
99
|
+
if (parts.length >= 3) {
|
|
100
|
+
managedSkillDirs.add(parts[2]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (entry.isDirectory() && !managedSkillDirs.has(entry.name)) {
|
|
107
|
+
customSkills.push(entry.name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return customSkills;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get status for a single client
|
|
116
|
+
* @param {string} clientName - Client name
|
|
117
|
+
* @returns {Promise<object>} Client status object
|
|
118
|
+
*/
|
|
119
|
+
async function getClientStatus(clientName) {
|
|
120
|
+
const clientsDir = getClientsDir();
|
|
121
|
+
const clientDir = join(clientsDir, clientName);
|
|
122
|
+
const basePath = getBaseTemplatePath();
|
|
123
|
+
const packageVersion = getPackageVersion();
|
|
124
|
+
|
|
125
|
+
// Read manifest
|
|
126
|
+
const manifest = await readManifest(clientDir);
|
|
127
|
+
const currentVersion = manifest.baseVersion;
|
|
128
|
+
|
|
129
|
+
// Get base template files for detecting modifications
|
|
130
|
+
const baseFiles = await getAllFiles(basePath);
|
|
131
|
+
|
|
132
|
+
// Detect modifications
|
|
133
|
+
const mods = await detectModifications(clientDir, manifest, baseFiles);
|
|
134
|
+
|
|
135
|
+
// Find custom skills
|
|
136
|
+
const customSkills = findCustomSkills(clientDir, manifest);
|
|
137
|
+
|
|
138
|
+
// Find config files
|
|
139
|
+
const configFiles = Object.entries(manifest.managedFiles)
|
|
140
|
+
.filter(([path, info]) => (info.managedType || 'update') === 'config')
|
|
141
|
+
.map(([path]) => path);
|
|
142
|
+
|
|
143
|
+
// Check if update available
|
|
144
|
+
const updateAvailable = currentVersion !== packageVersion;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
name: clientName,
|
|
148
|
+
version: currentVersion,
|
|
149
|
+
packageVersion,
|
|
150
|
+
updateAvailable,
|
|
151
|
+
managedCount: Object.keys(manifest.managedFiles).length,
|
|
152
|
+
modifiedFiles: mods.modified,
|
|
153
|
+
missingFiles: mods.missing,
|
|
154
|
+
customSkills,
|
|
155
|
+
conflicts: manifest.conflicts || [],
|
|
156
|
+
configFiles
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Main status command handler
|
|
162
|
+
* @param {object} options - Command options
|
|
163
|
+
* @param {string} [options.client] - Show only this client
|
|
164
|
+
*/
|
|
165
|
+
export default async function status(options = {}) {
|
|
166
|
+
const packageVersion = getPackageVersion();
|
|
167
|
+
const basePath = getBaseTemplatePath();
|
|
168
|
+
|
|
169
|
+
// Check base template exists
|
|
170
|
+
if (!existsSync(basePath)) {
|
|
171
|
+
logger.error('Template directory not found. Package may be corrupted.');
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Discover clients
|
|
177
|
+
let clients = discoverClients();
|
|
178
|
+
|
|
179
|
+
if (clients.length === 0) {
|
|
180
|
+
logger.info('No clients found.');
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log('To create a client:');
|
|
183
|
+
console.log(' ppcos init <client-name>');
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log('Or create main-config.json and run:');
|
|
186
|
+
console.log(' ppcos init-all');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Filter to specific client if requested
|
|
191
|
+
if (options.client) {
|
|
192
|
+
if (!clients.includes(options.client)) {
|
|
193
|
+
logger.error(`Client "${options.client}" not found.`);
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
clients = [options.client];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Print header
|
|
201
|
+
console.log('ppcos-hub/');
|
|
202
|
+
console.log('');
|
|
203
|
+
|
|
204
|
+
// Process each client
|
|
205
|
+
for (const clientName of clients) {
|
|
206
|
+
try {
|
|
207
|
+
const clientStatus = await getClientStatus(clientName);
|
|
208
|
+
|
|
209
|
+
// Client name
|
|
210
|
+
console.log(` ${clientStatus.name}`);
|
|
211
|
+
|
|
212
|
+
// Version line
|
|
213
|
+
if (clientStatus.updateAvailable) {
|
|
214
|
+
console.log(` Version: ${clientStatus.version} → ${clientStatus.packageVersion} available`);
|
|
215
|
+
} else {
|
|
216
|
+
console.log(` Version: ${clientStatus.version} (up to date)`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Managed files count
|
|
220
|
+
console.log(` Managed: ${clientStatus.managedCount} files`);
|
|
221
|
+
|
|
222
|
+
// Modified files
|
|
223
|
+
console.log(` Modified: ${clientStatus.modifiedFiles.length} files`);
|
|
224
|
+
for (const file of clientStatus.modifiedFiles) {
|
|
225
|
+
console.log(` - ${file}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Custom skills
|
|
229
|
+
const skillCount = clientStatus.customSkills.length;
|
|
230
|
+
if (skillCount === 0) {
|
|
231
|
+
console.log(' Custom: 0 skills');
|
|
232
|
+
} else if (skillCount === 1) {
|
|
233
|
+
console.log(` Custom: 1 skill (${clientStatus.customSkills[0]})`);
|
|
234
|
+
} else {
|
|
235
|
+
console.log(` Custom: ${skillCount} skills (${clientStatus.customSkills.join(', ')})`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Config files
|
|
239
|
+
const configCount = clientStatus.configFiles?.length || 0;
|
|
240
|
+
if (configCount > 0) {
|
|
241
|
+
console.log(` Config: ${configCount} files (never updated)`);
|
|
242
|
+
for (const file of clientStatus.configFiles) {
|
|
243
|
+
console.log(` - ${file}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Conflicts
|
|
248
|
+
console.log(` Conflicts: ${clientStatus.conflicts.length}`);
|
|
249
|
+
|
|
250
|
+
console.log('');
|
|
251
|
+
} catch (err) {
|
|
252
|
+
logger.error(`Failed to get status for ${clientName}: ${err.message}`);
|
|
253
|
+
console.log('');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Auth status
|
|
258
|
+
const auth = readAuth();
|
|
259
|
+
if (auth?.sessionToken) {
|
|
260
|
+
const expiresAt = new Date(auth.expiresAt);
|
|
261
|
+
const now = new Date();
|
|
262
|
+
const isExpired = expiresAt < now;
|
|
263
|
+
|
|
264
|
+
if (isExpired) {
|
|
265
|
+
console.log(`Auth: ${chalk.red('expired')} (${auth.email})`);
|
|
266
|
+
console.log(` Run: ${chalk.cyan('ppcos login')}`);
|
|
267
|
+
} else {
|
|
268
|
+
const hoursLeft = Math.floor((expiresAt - now) / (1000 * 60 * 60));
|
|
269
|
+
console.log(`Auth: ${chalk.green('active')} (${auth.email})`);
|
|
270
|
+
console.log(` Expires in ${hoursLeft}h`);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
console.log(`Auth: ${chalk.red('not logged in')} - run ${chalk.cyan('ppcos login')}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Package version footer
|
|
277
|
+
console.log('');
|
|
278
|
+
console.log(`Package version: ${packageVersion}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Export helpers for testing
|
|
282
|
+
export {
|
|
283
|
+
getPackageVersion,
|
|
284
|
+
getBaseTemplatePath,
|
|
285
|
+
getClientsDir,
|
|
286
|
+
discoverClients,
|
|
287
|
+
findCustomSkills,
|
|
288
|
+
getClientStatus
|
|
289
|
+
};
|