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
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Terms of Use
|
|
2
|
+
|
|
3
|
+
This software is licensed for personal and internal business use only under the PPC Mastery General Terms & Conditions. Use it to become better at your job. Don't use it to build things you sell to others.
|
|
4
|
+
|
|
5
|
+
Violations may be detected through embedded document fingerprints and will be pursued under Article 13 (Intellectual Property) of the PPC Mastery General Terms.
|
|
6
|
+
|
|
7
|
+
Full terms: https://www.ppcmastery.com/terms-and-conditions
|
|
8
|
+
|
|
9
|
+
© 2026 PPC Mastery B.V. All rights reserved.
|
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# ppcos-cli
|
|
2
|
+
|
|
3
|
+
CLI tool to distribute and manage Claude Code skills and agents for Google Ads workflows.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ppcos
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18+.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Create a hub folder and navigate to it
|
|
17
|
+
mkdir ppcos-hub && cd ppcos-hub
|
|
18
|
+
|
|
19
|
+
# Create main-config.json template
|
|
20
|
+
ppcos init
|
|
21
|
+
|
|
22
|
+
# Edit main-config.json with your client names, then:
|
|
23
|
+
ppcos init-all
|
|
24
|
+
|
|
25
|
+
# Check status of all clients
|
|
26
|
+
ppcos status
|
|
27
|
+
|
|
28
|
+
# Update all clients when new version is released
|
|
29
|
+
ppcos update
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### `ppcos init [client-name]`
|
|
35
|
+
|
|
36
|
+
Without args: create main-config.json template.
|
|
37
|
+
With client name: create a client workspace.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ppcos init # Create main-config.json template
|
|
41
|
+
ppcos init client-acme # Create client workspace
|
|
42
|
+
ppcos init client-acme --skip-config # Don't add to main-config.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Creates:
|
|
46
|
+
```
|
|
47
|
+
clients/client-acme/
|
|
48
|
+
├── .managed.json # Tracks managed files
|
|
49
|
+
├── .claude/
|
|
50
|
+
│ ├── skills/ # 6 Google Ads skills
|
|
51
|
+
│ ├── agents/ # 4 specialized agents
|
|
52
|
+
│ └── settings.local.json
|
|
53
|
+
├── CLAUDE.md # Client context template
|
|
54
|
+
├── config/ # API credentials
|
|
55
|
+
├── context/ # Client data
|
|
56
|
+
└── created/ # Generated assets
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `ppcos init-all`
|
|
60
|
+
|
|
61
|
+
Initialize all clients defined in main-config.json.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ppcos init-all
|
|
65
|
+
ppcos init-all --force # Reinitialize existing clients (backs up first)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**main-config.json example:**
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"version": "1.0",
|
|
72
|
+
"clients": [
|
|
73
|
+
{ "name": "client-acme", "enabled": true },
|
|
74
|
+
{ "name": "client-beta", "enabled": true },
|
|
75
|
+
{ "name": "client-gamma", "enabled": false }
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `ppcos update`
|
|
81
|
+
|
|
82
|
+
Update base skills in all clients while preserving custom work.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
ppcos update # Update all clients
|
|
86
|
+
ppcos update --client acme # Update specific client
|
|
87
|
+
ppcos update --dry-run # Show what would change
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Conflict handling:**
|
|
91
|
+
- Unchanged files: automatically updated
|
|
92
|
+
- Modified files: prompts with options (backup/skip/cancel)
|
|
93
|
+
- Custom files: never touched
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Updating client-acme (v1.0.0 → v1.1.0)
|
|
97
|
+
|
|
98
|
+
⚠ Modified files detected:
|
|
99
|
+
- .claude/skills/rsa-maker/SKILL.md
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
[1] Backup and overwrite (files saved to .backup/)
|
|
103
|
+
[2] Skip modified files (keep your changes)
|
|
104
|
+
[3] Cancel update
|
|
105
|
+
|
|
106
|
+
Choice [1/2/3]:
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `ppcos status`
|
|
110
|
+
|
|
111
|
+
Show version and modification status for all clients.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ppcos status
|
|
115
|
+
ppcos status --client acme # Show specific client only
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Output:
|
|
119
|
+
```
|
|
120
|
+
ppcos-hub/
|
|
121
|
+
|
|
122
|
+
client-acme
|
|
123
|
+
Version: 1.0.0 → 1.1.0 available
|
|
124
|
+
Managed: 42 files
|
|
125
|
+
Modified: 2 files
|
|
126
|
+
- .claude/skills/rsa-maker/SKILL.md
|
|
127
|
+
Custom: 1 skill
|
|
128
|
+
Conflicts: 0
|
|
129
|
+
|
|
130
|
+
client-beta
|
|
131
|
+
Version: 1.1.0 (up to date)
|
|
132
|
+
Managed: 42 files
|
|
133
|
+
Modified: 0 files
|
|
134
|
+
Custom: 0 skills
|
|
135
|
+
Conflicts: 0
|
|
136
|
+
|
|
137
|
+
Package version: 1.1.0
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Included Skills
|
|
141
|
+
|
|
142
|
+
| Skill | Description |
|
|
143
|
+
|-------|-------------|
|
|
144
|
+
| `gads-context` | Pull Google Ads account data (campaigns, ads, keywords) |
|
|
145
|
+
| `competitor-scraper` | Fetch competitor ads via DataForSEO API |
|
|
146
|
+
| `ads-context-gatherer` | Gather brand context from websites |
|
|
147
|
+
| `offer-angles` | Extract offer message angles for RSA composition |
|
|
148
|
+
| `rsa-maker` | Create Responsive Search Ads from offer angles |
|
|
149
|
+
| `search-term-analyzer` | Analyze search terms for keyword opportunities |
|
|
150
|
+
| `account-changelog` | Fetch account change history from Google Ads |
|
|
151
|
+
| `landing-page-builder` | Generate high-converting landing page wireframes |
|
|
152
|
+
| `ecom-page-builder` | Generate ecommerce page wireframes with product-first layouts |
|
|
153
|
+
|
|
154
|
+
## Included Agents
|
|
155
|
+
|
|
156
|
+
| Agent | Description |
|
|
157
|
+
|-------|-------------|
|
|
158
|
+
| `qs-decider` | Quality Score improvement coordinator |
|
|
159
|
+
| `ad-relevance-analyzer` | Analyze ad relevance issues |
|
|
160
|
+
| `expected-ctr-analyzer` | Analyze expected CTR issues |
|
|
161
|
+
| `landing-page-analyzer` | Analyze landing page experience |
|
|
162
|
+
|
|
163
|
+
## How It Works
|
|
164
|
+
|
|
165
|
+
### Managed vs Custom Files
|
|
166
|
+
|
|
167
|
+
- **Managed files**: Created by `init`, tracked in `.managed.json`, updated by CLI
|
|
168
|
+
- **Custom files**: Created by you, never touched by updates
|
|
169
|
+
|
|
170
|
+
Add your own skills to `.claude/skills/` - they'll never be modified.
|
|
171
|
+
|
|
172
|
+
### Update Safety
|
|
173
|
+
|
|
174
|
+
1. Calculates SHA256 checksum of each managed file
|
|
175
|
+
2. Compares with stored checksum in `.managed.json`
|
|
176
|
+
3. Unchanged files: updated silently
|
|
177
|
+
4. Modified files: user prompted (backup/skip/cancel)
|
|
178
|
+
5. Custom files: completely ignored
|
|
179
|
+
|
|
180
|
+
### Typical Workflow
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Setup (once)
|
|
184
|
+
mkdir ppcos-hub && cd ppcos-hub
|
|
185
|
+
ppcos init client-acme
|
|
186
|
+
cd clients/client-acme
|
|
187
|
+
|
|
188
|
+
# Daily work
|
|
189
|
+
# Edit CLAUDE.md with client context
|
|
190
|
+
# Run /ppcos to gather brand info
|
|
191
|
+
# Create ads with /rsa-maker
|
|
192
|
+
|
|
193
|
+
# When CLI updates
|
|
194
|
+
cd ../..
|
|
195
|
+
ppcos update
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Configuration
|
|
199
|
+
|
|
200
|
+
### config/.env.example
|
|
201
|
+
|
|
202
|
+
Template for API credentials. Copy to `.env` and fill in:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
GOOGLE_ADS_DEVELOPER_TOKEN=your_token
|
|
206
|
+
GOOGLE_ADS_CLIENT_ID=your_client_id
|
|
207
|
+
GOOGLE_ADS_CLIENT_SECRET=your_client_secret
|
|
208
|
+
GOOGLE_ADS_REFRESH_TOKEN=your_refresh_token
|
|
209
|
+
DATAFORSEO_USERNAME=your_username
|
|
210
|
+
DATAFORSEO_PASSWORD=your_password
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### config/ads-context.config.json
|
|
214
|
+
|
|
215
|
+
Per-client settings:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"customerId": "1234567890",
|
|
220
|
+
"competitors": ["competitor1.com", "competitor2.com"],
|
|
221
|
+
"targetLocations": ["United States"]
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Adding Skills to the Base
|
|
226
|
+
|
|
227
|
+
To add a new skill that gets distributed to all clients:
|
|
228
|
+
|
|
229
|
+
1. Create the skill folder in `.claude-base/.claude/skills/<skill-name>/` with at minimum a `SKILL.md`
|
|
230
|
+
2. Bump the package version:
|
|
231
|
+
```bash
|
|
232
|
+
npm version patch # 1.0.0 → 1.0.1
|
|
233
|
+
```
|
|
234
|
+
3. Push to GitHub
|
|
235
|
+
|
|
236
|
+
Existing clients will receive the new skill on their next `ppcos update`. New clients get it automatically via `ppcos init`.
|
|
237
|
+
|
|
238
|
+
The same applies to agents (`.claude-base/.claude/agents/`) and any other base template files.
|
|
239
|
+
|
|
240
|
+
**Why the version bump matters:** The update command compares `package.json` version against each client's stored version. Without a bump, `ppcos update` sees clients as up-to-date and skips them.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
Licensed for personal and internal business use only under the [PPC Mastery General Terms & Conditions](https://www.ppcmastery.com/terms-and-conditions).
|
|
245
|
+
|
|
246
|
+
© 2026 PPC Mastery B.V. All rights reserved.
|
package/bin/ppcos.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
// Import commands
|
|
9
|
+
import init from '../lib/commands/init.js';
|
|
10
|
+
import initAll from '../lib/commands/init-all.js';
|
|
11
|
+
import update from '../lib/commands/update.js';
|
|
12
|
+
import status from '../lib/commands/status.js';
|
|
13
|
+
import { login } from '../lib/commands/login.js';
|
|
14
|
+
import { logout } from '../lib/commands/logout.js';
|
|
15
|
+
import { whoami } from '../lib/commands/whoami.js';
|
|
16
|
+
import { requireAuth } from '../lib/utils/auth.js';
|
|
17
|
+
|
|
18
|
+
// Get package.json for version
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a command handler with authentication check
|
|
25
|
+
*/
|
|
26
|
+
function gated(fn) {
|
|
27
|
+
return async (...args) => {
|
|
28
|
+
const auth = requireAuth();
|
|
29
|
+
if (!auth) {
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
return fn(...args);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const program = new Command();
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.name('ppcos')
|
|
41
|
+
.description('CLI tool to manage Google Ads AI workflow skills and agents for Claude Code')
|
|
42
|
+
.version(pkg.version);
|
|
43
|
+
|
|
44
|
+
// login — NOT gated
|
|
45
|
+
program
|
|
46
|
+
.command('login')
|
|
47
|
+
.description('Log in with Circle.so membership (email + OTP)')
|
|
48
|
+
.action(login);
|
|
49
|
+
|
|
50
|
+
// logout — NOT gated
|
|
51
|
+
program
|
|
52
|
+
.command('logout')
|
|
53
|
+
.description('Log out and clear session')
|
|
54
|
+
.action(logout);
|
|
55
|
+
|
|
56
|
+
// whoami — NOT gated
|
|
57
|
+
program
|
|
58
|
+
.command('whoami')
|
|
59
|
+
.description('Show current authentication status')
|
|
60
|
+
.action(whoami);
|
|
61
|
+
|
|
62
|
+
// init [client-name]
|
|
63
|
+
program
|
|
64
|
+
.command('init [client-name]')
|
|
65
|
+
.description('Create main-config.json template, or with <client-name> create a client workspace')
|
|
66
|
+
.option('--skip-config', 'Don\'t add client to main-config.json')
|
|
67
|
+
.action(gated(init));
|
|
68
|
+
|
|
69
|
+
// init-all
|
|
70
|
+
program
|
|
71
|
+
.command('init-all')
|
|
72
|
+
.description('Initialize all clients defined in main-config.json')
|
|
73
|
+
.option('--force', 'Reinitialize existing clients (backs up first)')
|
|
74
|
+
.action(gated(initAll));
|
|
75
|
+
|
|
76
|
+
// update
|
|
77
|
+
program
|
|
78
|
+
.command('update')
|
|
79
|
+
.description('Update base skills in all clients while preserving custom work')
|
|
80
|
+
.option('--client <name>', 'Update only this client')
|
|
81
|
+
.option('--dry-run', 'Show changes without applying')
|
|
82
|
+
.action(gated(update));
|
|
83
|
+
|
|
84
|
+
// status
|
|
85
|
+
program
|
|
86
|
+
.command('status')
|
|
87
|
+
.description('Show version and modification status for all clients')
|
|
88
|
+
.option('--client <name>', 'Show only this client')
|
|
89
|
+
.action(gated(status));
|
|
90
|
+
|
|
91
|
+
program.parse();
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init-all command - Initialize all clients from main-config.json
|
|
3
|
+
*
|
|
4
|
+
* Usage: ppcos init-all [--force]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { readFile, mkdir, rename } from 'node:fs/promises';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { validateConfig } from '../utils/validation.js';
|
|
12
|
+
import { calculateChecksum } from '../utils/checksum.js';
|
|
13
|
+
import { createManifest, writeManifest, manifestExists } from '../utils/manifest.js';
|
|
14
|
+
import { getAllFiles, copyFileWithDirs, ensureDir } from '../utils/fs-helpers.js';
|
|
15
|
+
import logger from '../utils/logger.js';
|
|
16
|
+
|
|
17
|
+
// Get package root directory
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get package version from package.json
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function getPackageVersion() {
|
|
27
|
+
const pkgPath = join(PACKAGE_ROOT, 'package.json');
|
|
28
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
29
|
+
return pkg.version;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get path to .claude-base template directory
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function getBaseTemplatePath() {
|
|
37
|
+
return join(PACKAGE_ROOT, '.claude-base');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get config file path
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function getConfigPath() {
|
|
45
|
+
return join(process.cwd(), 'main-config.json');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get clients directory path from config or default
|
|
50
|
+
* @param {object} config
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function getClientsDir(config) {
|
|
54
|
+
const dir = config?.settings?.clientsDirectory || 'clients';
|
|
55
|
+
return join(process.cwd(), dir);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load and validate main-config.json
|
|
60
|
+
* @returns {{ config: object, errors: string[] } | null}
|
|
61
|
+
*/
|
|
62
|
+
async function loadConfig() {
|
|
63
|
+
const configPath = getConfigPath();
|
|
64
|
+
|
|
65
|
+
if (!existsSync(configPath)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const content = await readFile(configPath, 'utf8');
|
|
71
|
+
const config = JSON.parse(content);
|
|
72
|
+
const validation = validateConfig(config);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
config,
|
|
76
|
+
errors: validation.errors
|
|
77
|
+
};
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
config: null,
|
|
81
|
+
errors: [`JSON parse error: ${err.message}`]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create backup of existing client directory
|
|
88
|
+
* @param {string} clientDir
|
|
89
|
+
* @returns {Promise<string>} Backup path
|
|
90
|
+
*/
|
|
91
|
+
async function backupClient(clientDir) {
|
|
92
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
93
|
+
const backupDir = join(clientDir, '.backup', timestamp);
|
|
94
|
+
const tempBackupDir = join(dirname(clientDir), `.backup-temp-${timestamp}`);
|
|
95
|
+
|
|
96
|
+
// Move current contents to temp location
|
|
97
|
+
await rename(clientDir, tempBackupDir);
|
|
98
|
+
|
|
99
|
+
// Create new client directory
|
|
100
|
+
await mkdir(clientDir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Move temp to .backup inside new client dir
|
|
103
|
+
const backupPath = join(clientDir, '.backup', timestamp);
|
|
104
|
+
await mkdir(join(clientDir, '.backup'), { recursive: true });
|
|
105
|
+
await rename(tempBackupDir, backupPath);
|
|
106
|
+
|
|
107
|
+
return backupPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Initialize a single client
|
|
112
|
+
* @param {string} clientName
|
|
113
|
+
* @param {string} clientsDir
|
|
114
|
+
* @param {object} options
|
|
115
|
+
* @returns {Promise<{ status: 'initialized' | 'skipped' | 'failed', message?: string }>}
|
|
116
|
+
*/
|
|
117
|
+
async function initializeClient(clientName, clientsDir, options = {}) {
|
|
118
|
+
const clientDir = join(clientsDir, clientName);
|
|
119
|
+
const basePath = getBaseTemplatePath();
|
|
120
|
+
const version = getPackageVersion();
|
|
121
|
+
|
|
122
|
+
// Check if exists
|
|
123
|
+
if (existsSync(clientDir)) {
|
|
124
|
+
if (options.force) {
|
|
125
|
+
// Backup existing
|
|
126
|
+
try {
|
|
127
|
+
await backupClient(clientDir);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return { status: 'failed', message: `Backup failed: ${err.message}` };
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
return { status: 'skipped', message: 'already exists' };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Copy files and calculate checksums
|
|
138
|
+
const baseFiles = await getAllFiles(basePath);
|
|
139
|
+
const managedFiles = {};
|
|
140
|
+
|
|
141
|
+
for (const relativePath of baseFiles) {
|
|
142
|
+
const srcPath = join(basePath, relativePath);
|
|
143
|
+
const destPath = join(clientDir, relativePath);
|
|
144
|
+
|
|
145
|
+
await copyFileWithDirs(srcPath, destPath);
|
|
146
|
+
|
|
147
|
+
const checksum = await calculateChecksum(destPath);
|
|
148
|
+
managedFiles[relativePath] = {
|
|
149
|
+
checksum,
|
|
150
|
+
version
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Generate and write .managed.json
|
|
155
|
+
const manifest = createManifest(version, managedFiles);
|
|
156
|
+
await writeManifest(clientDir, manifest);
|
|
157
|
+
|
|
158
|
+
return { status: 'initialized' };
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return { status: 'failed', message: err.message };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Main init-all command handler
|
|
166
|
+
* @param {object} options - Command options
|
|
167
|
+
* @param {boolean} options.force - Reinitialize existing clients
|
|
168
|
+
*/
|
|
169
|
+
export default async function initAll(options = {}) {
|
|
170
|
+
// 1. Load main-config.json
|
|
171
|
+
const result = await loadConfig();
|
|
172
|
+
|
|
173
|
+
if (result === null) {
|
|
174
|
+
logger.error('No main-config.json found in current directory.');
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (result.errors.length > 0) {
|
|
180
|
+
logger.error('Invalid main-config.json:');
|
|
181
|
+
for (const err of result.errors) {
|
|
182
|
+
console.log(` - ${err}`);
|
|
183
|
+
}
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const config = result.config;
|
|
189
|
+
const clientsDir = getClientsDir(config);
|
|
190
|
+
|
|
191
|
+
// Filter to enabled clients
|
|
192
|
+
const enabledClients = config.clients.filter(c => c.enabled);
|
|
193
|
+
|
|
194
|
+
if (enabledClients.length === 0) {
|
|
195
|
+
logger.warn('No enabled clients found in main-config.json');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(`Found ${enabledClients.length} client${enabledClients.length === 1 ? '' : 's'} in main-config.json`);
|
|
200
|
+
console.log('');
|
|
201
|
+
|
|
202
|
+
// 2. Process each client
|
|
203
|
+
const results = {
|
|
204
|
+
initialized: 0,
|
|
205
|
+
skipped: 0,
|
|
206
|
+
failed: 0
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const client of enabledClients) {
|
|
210
|
+
process.stdout.write(`Initializing ${client.name}... `);
|
|
211
|
+
|
|
212
|
+
const initResult = await initializeClient(client.name, clientsDir, options);
|
|
213
|
+
|
|
214
|
+
if (initResult.status === 'initialized') {
|
|
215
|
+
logger.success('');
|
|
216
|
+
results.initialized++;
|
|
217
|
+
} else if (initResult.status === 'skipped') {
|
|
218
|
+
console.log(`Skipped (${initResult.message})`);
|
|
219
|
+
results.skipped++;
|
|
220
|
+
} else {
|
|
221
|
+
logger.error(initResult.message || 'Unknown error');
|
|
222
|
+
results.failed++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 3. Display summary
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log('Summary:');
|
|
229
|
+
console.log(` Initialized: ${results.initialized}`);
|
|
230
|
+
console.log(` Skipped: ${results.skipped}`);
|
|
231
|
+
console.log(` Failed: ${results.failed}`);
|
|
232
|
+
|
|
233
|
+
if (results.failed > 0) {
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Export helpers for testing
|
|
239
|
+
export {
|
|
240
|
+
getPackageVersion,
|
|
241
|
+
getBaseTemplatePath,
|
|
242
|
+
getConfigPath,
|
|
243
|
+
getClientsDir,
|
|
244
|
+
loadConfig,
|
|
245
|
+
initializeClient,
|
|
246
|
+
backupClient
|
|
247
|
+
};
|