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 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
+ };