@daemux/store-automator 0.10.62 → 0.10.64
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/.claude-plugin/marketplace.json +2 -2
- package/README.md +38 -23
- package/bin/cli.mjs +10 -3
- package/package.json +1 -1
- package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
- package/src/backup.mjs +99 -0
- package/src/install.mjs +101 -80
- package/src/managed-settings.mjs +88 -0
- package/src/settings.mjs +0 -37
- package/src/templates.mjs +51 -4
- package/src/uninstall.mjs +152 -18
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "App Store & Google Play automation for Flutter apps",
|
|
8
|
-
"version": "0.10.
|
|
8
|
+
"version": "0.10.64"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "store-automator",
|
|
13
13
|
"source": "./plugins/store-automator",
|
|
14
14
|
"description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
|
|
15
|
-
"version": "0.10.
|
|
15
|
+
"version": "0.10.64",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/README.md
CHANGED
|
@@ -20,30 +20,46 @@ Plus CI/CD templates for GitHub Actions, Fastlane, web pages, and scripts.
|
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
+
**Project (default):** Full install into your Flutter project -- agents, CI/CD templates, MCP servers, and interactive setup:
|
|
24
|
+
|
|
23
25
|
```bash
|
|
24
26
|
cd your-flutter-project
|
|
25
|
-
|
|
27
|
+
npx --yes @daemux/store-automator
|
|
26
28
|
```
|
|
27
29
|
|
|
28
|
-
The
|
|
30
|
+
The installer runs an interactive setup with five sections:
|
|
29
31
|
|
|
30
32
|
1. **App Identity** -- App name, bundle ID, package name, SKU, Apple ID
|
|
31
|
-
2. **Credentials** --
|
|
33
|
+
2. **Credentials** -- App Store Connect API key, Google Play service account, Android keystore, Match code signing
|
|
32
34
|
3. **Store Settings** -- iOS categories/pricing, Android track/rollout, metadata languages
|
|
33
35
|
4. **Web Settings** -- Domain, colors, company info, legal jurisdiction
|
|
34
|
-
5. **MCP Tokens** -- Stitch and Cloudflare API keys (optional
|
|
35
|
-
|
|
36
|
-
All values are written to `ci.config.yaml`. The installer also:
|
|
36
|
+
5. **MCP Tokens** -- Stitch and Cloudflare API keys (optional)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
- Installs `.claude/CLAUDE.md` with your app name and agent configurations
|
|
40
|
-
- Copies CI/CD templates (Fastlane, scripts, web pages, GitHub Actions)
|
|
41
|
-
- Configures `.claude/settings.json` with required env vars
|
|
42
|
-
- Runs post-install guides for GitHub repo setup, secrets, and Firebase
|
|
38
|
+
All values are written to `ci.config.yaml`. The installer also configures `.mcp.json`, installs `.claude/CLAUDE.md`, copies CI/CD templates, and configures `.claude/settings.json`.
|
|
43
39
|
|
|
44
40
|
Re-running the installer reads existing `ci.config.yaml` values as defaults, so you can update individual fields without re-entering everything.
|
|
45
41
|
|
|
46
|
-
|
|
42
|
+
**Global (agents only):** Install the plugin into `~/.claude` for use across all projects, without touching the current directory:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx --yes @daemux/store-automator --global
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Global install:
|
|
49
|
+
|
|
50
|
+
- Registers the marketplace and installs the plugin at user scope
|
|
51
|
+
- Writes `~/.claude/CLAUDE.md` with agent configurations
|
|
52
|
+
- Adds required env vars and statusLine to `~/.claude/settings.json`
|
|
53
|
+
|
|
54
|
+
Global install does NOT:
|
|
55
|
+
|
|
56
|
+
- Create `ci.config.yaml`, `fastlane/`, `scripts/`, `web/`, `Gemfile`, `.github/workflows/`, or any other project files
|
|
57
|
+
- Write or modify `.mcp.json` in the current directory
|
|
58
|
+
- Run any interactive prompts or post-install guides
|
|
59
|
+
|
|
60
|
+
Use project install for any Flutter app you are actually publishing. Global install is for agent-only access from arbitrary directories.
|
|
61
|
+
|
|
62
|
+
## After Installation _(project scope only)_
|
|
47
63
|
|
|
48
64
|
1. Add any credential files not configured during the guided setup:
|
|
49
65
|
- `creds/AuthKey.p8` -- Apple App Store Connect API key
|
|
@@ -52,7 +68,7 @@ Re-running the installer reads existing `ci.config.yaml` values as defaults, so
|
|
|
52
68
|
2. Verify `ci.config.yaml` has all required values filled in
|
|
53
69
|
3. Start Claude Code and use the agents
|
|
54
70
|
|
|
55
|
-
## Manual Setup
|
|
71
|
+
## Manual Setup _(project scope only)_
|
|
56
72
|
|
|
57
73
|
If postinstall was skipped (CI environment), run manually:
|
|
58
74
|
|
|
@@ -240,19 +256,18 @@ web:
|
|
|
240
256
|
google_play_url: "" # Filled after first Android publish
|
|
241
257
|
```
|
|
242
258
|
|
|
243
|
-
##
|
|
259
|
+
## Uninstall
|
|
244
260
|
|
|
245
|
-
|
|
261
|
+
**Project uninstall:** Removes CI templates, `ci.config.yaml`, `.mcp.json` entries, `.claude/CLAUDE.md`, and settings from the current project. Marketplace files remain in `~/.claude/plugins/`.
|
|
246
262
|
|
|
247
263
|
```bash
|
|
248
|
-
npx @daemux/store-automator
|
|
264
|
+
npx --yes @daemux/store-automator --uninstall
|
|
249
265
|
```
|
|
250
266
|
|
|
251
|
-
|
|
267
|
+
**Global uninstall:** Removes the plugin from `~/.claude` and clears the marketplace. Does NOT touch the current project's `ci.config.yaml`, CI templates, or `.mcp.json`.
|
|
252
268
|
|
|
253
269
|
```bash
|
|
254
|
-
npx @daemux/store-automator
|
|
255
|
-
npx @daemux/store-automator -g -u # global scope
|
|
270
|
+
npx --yes @daemux/store-automator --global --uninstall
|
|
256
271
|
```
|
|
257
272
|
|
|
258
273
|
## Agents
|
|
@@ -269,7 +284,7 @@ Designs complete app UI screens, creates ASO-optimized store screenshots for all
|
|
|
269
284
|
|
|
270
285
|
Reviews all metadata, screenshots, privacy policy, and IAP configuration against Apple and Google guidelines. Returns APPROVED or REJECTED with specific issues.
|
|
271
286
|
|
|
272
|
-
## CI/CD Templates
|
|
287
|
+
## CI/CD Templates _(project scope only)_
|
|
273
288
|
|
|
274
289
|
The package installs these templates to your project:
|
|
275
290
|
|
|
@@ -282,7 +297,7 @@ The package installs these templates to your project:
|
|
|
282
297
|
| `web/` | Marketing, privacy, terms, and support page templates |
|
|
283
298
|
| `Gemfile` | Ruby gems for Fastlane |
|
|
284
299
|
|
|
285
|
-
## Workflow
|
|
300
|
+
## Workflow _(project scope only)_
|
|
286
301
|
|
|
287
302
|
1. Install the package (interactive setup fills `ci.config.yaml`)
|
|
288
303
|
2. Add any remaining credential files
|
|
@@ -291,7 +306,7 @@ The package installs these templates to your project:
|
|
|
291
306
|
5. Use `appstore-reviewer` to verify compliance
|
|
292
307
|
6. Push to GitHub -- GitHub Actions builds and publishes automatically
|
|
293
308
|
|
|
294
|
-
## MCP Servers
|
|
309
|
+
## MCP Servers _(project scope only)_
|
|
295
310
|
|
|
296
311
|
The package configures these MCP servers in `.mcp.json`:
|
|
297
312
|
|
|
@@ -302,7 +317,7 @@ The package configures these MCP servers in `.mcp.json`:
|
|
|
302
317
|
| stitch | AI design tool for screenshot generation | `STITCH_API_KEY` |
|
|
303
318
|
| cloudflare | Cloudflare Pages deployment | `CLOUDFLARE_API_TOKEN` + Account ID |
|
|
304
319
|
|
|
305
|
-
## Idempotency
|
|
320
|
+
## Idempotency _(project scope only)_
|
|
306
321
|
|
|
307
322
|
The installer is idempotent. Re-running it reads existing values from `ci.config.yaml` as defaults for each prompt. This means you can:
|
|
308
323
|
|
package/bin/cli.mjs
CHANGED
|
@@ -9,8 +9,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
9
9
|
const __dirname = dirname(__filename);
|
|
10
10
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
11
11
|
|
|
12
|
-
const notifier = updateNotifier({ pkg });
|
|
13
|
-
|
|
14
12
|
const args = process.argv.slice(2);
|
|
15
13
|
let scope = 'project';
|
|
16
14
|
let action = 'install';
|
|
@@ -146,6 +144,13 @@ Examples:
|
|
|
146
144
|
}
|
|
147
145
|
}
|
|
148
146
|
|
|
147
|
+
if (scope === 'user' && cliTokens.githubActions) {
|
|
148
|
+
console.error('Error: --github-actions and --global are mutually exclusive.');
|
|
149
|
+
console.error(' --global targets ~/.claude (no CI templates).');
|
|
150
|
+
console.error(' --github-actions targets a project repo. Pick one.');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
149
154
|
if (cliTokens.githubActions) {
|
|
150
155
|
const missing = [];
|
|
151
156
|
if (!cliTokens.matchDeployKey) missing.push('--match-deploy-key');
|
|
@@ -163,7 +168,9 @@ if (cliTokens.githubActions) {
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
|
|
171
|
+
if (!isPostinstall) {
|
|
172
|
+
updateNotifier({ pkg }).notify();
|
|
173
|
+
}
|
|
167
174
|
|
|
168
175
|
try {
|
|
169
176
|
if (action === 'uninstall') {
|
package/package.json
CHANGED
package/src/backup.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync, mkdirSync, readFileSync, writeFileSync,
|
|
3
|
+
readdirSync, rmSync, statSync,
|
|
4
|
+
} from 'node:fs';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
const BACKUP_ROOT = join(homedir(), '.claude', 'backups', 'store-automator');
|
|
10
|
+
const RETAIN_COUNT = 10;
|
|
11
|
+
|
|
12
|
+
function utcTimestamp() {
|
|
13
|
+
return new Date().toISOString().slice(0, 19).replace(/:/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sha256(buf) {
|
|
17
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeBackupFile(filePath, contentBuf) {
|
|
21
|
+
const ts = utcTimestamp();
|
|
22
|
+
const dir = join(BACKUP_ROOT, ts);
|
|
23
|
+
const dest = join(dir, `${basename(filePath)}.bak`);
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
writeFileSync(dest, contentBuf);
|
|
26
|
+
console.log(`Backup written to: ${dest}`);
|
|
27
|
+
return dest;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function backupIfChanging(filePath, newContent) {
|
|
31
|
+
if (!existsSync(filePath)) {
|
|
32
|
+
return { backedUp: false, path: null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const current = readFileSync(filePath);
|
|
36
|
+
const incoming = Buffer.isBuffer(newContent) ? newContent : Buffer.from(newContent, 'utf8');
|
|
37
|
+
if (sha256(current) === sha256(incoming)) {
|
|
38
|
+
return { backedUp: false, path: null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const dest = writeBackupFile(filePath, current);
|
|
42
|
+
return { backedUp: true, path: dest };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Snapshot arbitrary content to a timestamped backup. Used for post-hoc backups
|
|
46
|
+
// when the caller only has access to pre-mutation content after the fact
|
|
47
|
+
// (e.g. functions that mutate the target file in place).
|
|
48
|
+
// No-op if preContent already matches postContent (nothing really changed).
|
|
49
|
+
export function backupSnapshot(filePath, preContent, postContent) {
|
|
50
|
+
if (preContent == null) return { backedUp: false, path: null };
|
|
51
|
+
if (preContent === postContent) return { backedUp: false, path: null };
|
|
52
|
+
const buf = Buffer.isBuffer(preContent) ? preContent : Buffer.from(preContent, 'utf8');
|
|
53
|
+
const dest = writeBackupFile(filePath, buf);
|
|
54
|
+
return { backedUp: true, path: dest };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function listBackupEntries() {
|
|
58
|
+
if (!existsSync(BACKUP_ROOT)) return [];
|
|
59
|
+
const entries = [];
|
|
60
|
+
for (const tsDir of readdirSync(BACKUP_ROOT)) {
|
|
61
|
+
const tsPath = join(BACKUP_ROOT, tsDir);
|
|
62
|
+
let s;
|
|
63
|
+
try { s = statSync(tsPath); } catch { continue; }
|
|
64
|
+
if (!s.isDirectory()) continue;
|
|
65
|
+
for (const file of readdirSync(tsPath)) {
|
|
66
|
+
const fullPath = join(tsPath, file);
|
|
67
|
+
entries.push({ basename: file, ts: tsDir, fullPath, tsDir: tsPath });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return entries;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function pruneBackups() {
|
|
74
|
+
const entries = listBackupEntries();
|
|
75
|
+
if (entries.length === 0) return;
|
|
76
|
+
|
|
77
|
+
const byBasename = new Map();
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
if (!byBasename.has(e.basename)) byBasename.set(e.basename, []);
|
|
80
|
+
byBasename.get(e.basename).push(e);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const toDelete = [];
|
|
84
|
+
for (const group of byBasename.values()) {
|
|
85
|
+
group.sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0));
|
|
86
|
+
toDelete.push(...group.slice(RETAIN_COUNT));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const e of toDelete) {
|
|
90
|
+
try { rmSync(e.fullPath, { force: true }); } catch { /* ignore */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const touchedTsDirs = new Set(toDelete.map((e) => e.tsDir));
|
|
94
|
+
for (const tsDir of touchedTsDirs) {
|
|
95
|
+
try {
|
|
96
|
+
if (readdirSync(tsDir).length === 0) rmSync(tsDir, { recursive: true, force: true });
|
|
97
|
+
} catch { /* ignore */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/install.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, rmSync, cpSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, rmSync, cpSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { execSync } from 'node:child_process';
|
|
@@ -13,6 +13,8 @@ import { getMcpServers, writeMcpJson } from './mcp-setup.mjs';
|
|
|
13
13
|
import { installClaudeMd, installCiTemplates, installFirebaseTemplates } from './templates.mjs';
|
|
14
14
|
import { readCiConfig, writeCiFields, writeCiLanguages, writeMatchConfig, isPlaceholder } from './ci-config.mjs';
|
|
15
15
|
import { installGitHubActionsPath } from './install-paths.mjs';
|
|
16
|
+
import { backupSnapshot, pruneBackups } from './backup.mjs';
|
|
17
|
+
import { writeManagedSettings } from './managed-settings.mjs';
|
|
16
18
|
|
|
17
19
|
function checkClaudeCli() {
|
|
18
20
|
const result = exec('command -v claude') || exec('which claude');
|
|
@@ -152,7 +154,55 @@ function printNextSteps(prompted) {
|
|
|
152
154
|
}
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
async function withReadline(fn) {
|
|
158
|
+
const { createInterface } = await import('node:readline');
|
|
159
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
160
|
+
try {
|
|
161
|
+
return await fn(rl);
|
|
162
|
+
} finally {
|
|
163
|
+
rl.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readIfExists(filePath) {
|
|
168
|
+
return existsSync(filePath) ? readFileSync(filePath, 'utf8') : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function configureScopedSettings(baseDir, packageDir, appName, scopeLabel) {
|
|
172
|
+
ensureDir(baseDir);
|
|
173
|
+
installClaudeMd(join(baseDir, 'CLAUDE.md'), packageDir, appName);
|
|
174
|
+
console.log(`Configuring ${scopeLabel} settings...`);
|
|
175
|
+
const settingsPath = join(baseDir, 'settings.json');
|
|
176
|
+
|
|
177
|
+
// Snapshot pre-mutation state so we can back it up if inject* changes anything.
|
|
178
|
+
// inject* functions mutate in place (ensureFile creates {} when missing), so the
|
|
179
|
+
// backup must capture state before they run and be written only if a real change
|
|
180
|
+
// occurred — otherwise re-running install would churn backups on every invocation.
|
|
181
|
+
const preContent = readIfExists(settingsPath);
|
|
182
|
+
|
|
183
|
+
injectEnvVars(settingsPath);
|
|
184
|
+
injectStatusLine(settingsPath);
|
|
185
|
+
|
|
186
|
+
const postContent = readIfExists(settingsPath) ?? '';
|
|
187
|
+
backupSnapshot(settingsPath, preContent, postContent);
|
|
188
|
+
|
|
189
|
+
writeManagedSettings(baseDir);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function printGlobalNote() {
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log('Note: Global install registers the marketplace and installs ~/.claude/CLAUDE.md only.');
|
|
195
|
+
console.log('To generate CI/CD templates, MCP config, and ci.config.yaml, run `npx @daemux/store-automator` inside a project directory.');
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
|
|
199
|
+
const isGlobal = scope === 'user';
|
|
200
|
+
|
|
201
|
+
if (isPostinstall && isGlobal) {
|
|
202
|
+
console.log('Skipping postinstall in global scope.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
156
206
|
checkClaudeCli();
|
|
157
207
|
|
|
158
208
|
console.log('Installing/updating Daemux Store Automator...');
|
|
@@ -160,102 +210,73 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
|
|
|
160
210
|
const isGitHubActions = Boolean(cliTokens.githubActions);
|
|
161
211
|
const nonInteractive = Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
|
|
162
212
|
const projectDir = process.cwd();
|
|
163
|
-
const oldVersion = readMarketplaceVersion();
|
|
164
213
|
const packageDir = getPackageDir();
|
|
214
|
+
const oldVersion = readMarketplaceVersion();
|
|
165
215
|
|
|
166
|
-
// 1. Copy plugin files + register marketplace
|
|
167
216
|
copyPluginFiles(packageDir);
|
|
168
217
|
clearCache();
|
|
169
218
|
registerMarketplace();
|
|
170
219
|
runClaudeInstall(scope);
|
|
171
|
-
|
|
172
220
|
const newVersion = readMarketplaceVersion('unknown');
|
|
173
221
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const currentConfig = readCiConfig(projectDir);
|
|
180
|
-
|
|
181
|
-
// 4. Run interactive prompts (or use CLI flags / skip in non-interactive)
|
|
182
|
-
let prompted;
|
|
183
|
-
if (isGitHubActions) {
|
|
184
|
-
prompted = {
|
|
185
|
-
bundleId: cliTokens.bundleId ?? '',
|
|
186
|
-
matchDeployKeyPath: cliTokens.matchDeployKey,
|
|
187
|
-
matchGitUrl: cliTokens.matchGitUrl,
|
|
188
|
-
};
|
|
189
|
-
} else if (nonInteractive) {
|
|
190
|
-
prompted = { ...cliTokens };
|
|
191
|
-
} else {
|
|
192
|
-
const { createInterface } = await import('node:readline');
|
|
193
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
194
|
-
try {
|
|
195
|
-
prompted = await promptAll(rl, cliTokens, currentConfig, projectDir);
|
|
196
|
-
} finally {
|
|
197
|
-
rl.close();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 5. Write all prompted values to ci.config.yaml
|
|
202
|
-
const ciFields = mapPromptsToCiFields(prompted);
|
|
203
|
-
const wrote = writeCiFields(projectDir, ciFields);
|
|
204
|
-
if (wrote) console.log('Configuration written to ci.config.yaml');
|
|
205
|
-
|
|
206
|
-
if (prompted.matchDeployKeyPath || prompted.matchGitUrl) {
|
|
207
|
-
const wroteMatch = writeMatchConfig(projectDir, {
|
|
208
|
-
deployKeyPath: prompted.matchDeployKeyPath,
|
|
209
|
-
gitUrl: prompted.matchGitUrl,
|
|
210
|
-
});
|
|
211
|
-
if (wroteMatch) console.log('Match credentials written to ci.config.yaml');
|
|
212
|
-
}
|
|
222
|
+
let prompted = {};
|
|
223
|
+
if (!isGlobal) {
|
|
224
|
+
installCiTemplates(projectDir, packageDir);
|
|
225
|
+
installFirebaseTemplates(projectDir, packageDir);
|
|
226
|
+
const currentConfig = readCiConfig(projectDir);
|
|
213
227
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
if (isGitHubActions) {
|
|
229
|
+
prompted = {
|
|
230
|
+
bundleId: cliTokens.bundleId ?? '',
|
|
231
|
+
matchDeployKeyPath: cliTokens.matchDeployKey,
|
|
232
|
+
matchGitUrl: cliTokens.matchGitUrl,
|
|
233
|
+
};
|
|
234
|
+
} else if (nonInteractive) {
|
|
235
|
+
prompted = { ...cliTokens };
|
|
236
|
+
} else {
|
|
237
|
+
prompted = await withReadline((rl) => promptAll(rl, cliTokens, currentConfig, projectDir));
|
|
221
238
|
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// 7. Configure MCP, CLAUDE.md, settings
|
|
225
|
-
if (!isGitHubActions) {
|
|
226
|
-
const servers = getMcpServers(prompted);
|
|
227
|
-
writeMcpJson(projectDir, servers);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const baseDir = scope === 'user'
|
|
231
|
-
? join(homedir(), '.claude')
|
|
232
|
-
: join(process.cwd(), '.claude');
|
|
233
239
|
|
|
234
|
-
|
|
240
|
+
if (writeCiFields(projectDir, mapPromptsToCiFields(prompted))) {
|
|
241
|
+
console.log('Configuration written to ci.config.yaml');
|
|
242
|
+
}
|
|
235
243
|
|
|
236
|
-
|
|
244
|
+
if (prompted.matchDeployKeyPath || prompted.matchGitUrl) {
|
|
245
|
+
const wroteMatch = writeMatchConfig(projectDir, {
|
|
246
|
+
deployKeyPath: prompted.matchDeployKeyPath,
|
|
247
|
+
gitUrl: prompted.matchGitUrl,
|
|
248
|
+
});
|
|
249
|
+
if (wroteMatch) console.log('Match credentials written to ci.config.yaml');
|
|
250
|
+
}
|
|
237
251
|
|
|
238
|
-
|
|
252
|
+
if (prompted.languages) {
|
|
253
|
+
const langStr = Array.isArray(prompted.languages) ? prompted.languages.join(',') : prompted.languages;
|
|
254
|
+
if (writeCiLanguages(projectDir, langStr)) {
|
|
255
|
+
console.log('Languages updated in ci.config.yaml');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const settingsPath = join(baseDir, 'settings.json');
|
|
243
|
-
injectEnvVars(settingsPath);
|
|
244
|
-
injectStatusLine(settingsPath);
|
|
259
|
+
if (!isGitHubActions) writeMcpJson(projectDir, getMcpServers(prompted));
|
|
260
|
+
installGitHubActionsPath(projectDir, packageDir, prompted);
|
|
245
261
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const { runPostInstallGuides } = await import('./prompts/store-settings.mjs');
|
|
252
|
-
await runPostInstallGuides(guideRl, currentConfig);
|
|
253
|
-
} finally {
|
|
254
|
-
guideRl.close();
|
|
262
|
+
if (!isGitHubActions && !nonInteractive) {
|
|
263
|
+
await withReadline(async (rl) => {
|
|
264
|
+
const { runPostInstallGuides } = await import('./prompts/store-settings.mjs');
|
|
265
|
+
await runPostInstallGuides(rl, currentConfig);
|
|
266
|
+
});
|
|
255
267
|
}
|
|
256
268
|
}
|
|
257
269
|
|
|
258
|
-
|
|
270
|
+
const baseDir = isGlobal ? join(homedir(), '.claude') : join(projectDir, '.claude');
|
|
271
|
+
const appName = prompted.appName || (isGlobal ? 'your app' : undefined);
|
|
272
|
+
configureScopedSettings(baseDir, packageDir, appName, isGlobal ? 'global' : 'project');
|
|
273
|
+
|
|
274
|
+
pruneBackups();
|
|
275
|
+
|
|
259
276
|
printSummary(scope, oldVersion, newVersion);
|
|
260
|
-
|
|
277
|
+
if (isGlobal) {
|
|
278
|
+
printGlobalNote();
|
|
279
|
+
} else {
|
|
280
|
+
printNextSteps(prompted);
|
|
281
|
+
}
|
|
261
282
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const LEDGER_FILENAME = 'store-automator-managed-settings.json';
|
|
5
|
+
|
|
6
|
+
export const HISTORICAL_ENV_VALUES = {
|
|
7
|
+
'CLAUDE_CODE_ENABLE_TASKS': 'true',
|
|
8
|
+
'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS': '1',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const HISTORICAL_ENV_KEYS = Object.keys(HISTORICAL_ENV_VALUES);
|
|
12
|
+
|
|
13
|
+
// Ledger is scoped to the settings.json baseDir (e.g. ~/.claude for global or
|
|
14
|
+
// <project>/.claude for project). Keeping it adjacent to the settings file it
|
|
15
|
+
// describes prevents global/project scope overlap.
|
|
16
|
+
export function getLedgerPath(baseDir) {
|
|
17
|
+
return join(baseDir, LEDGER_FILENAME);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getSettingsPath(baseDir) {
|
|
21
|
+
return join(baseDir, 'settings.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sortedStringify(value) {
|
|
25
|
+
return JSON.stringify(value, (_, v) => {
|
|
26
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const key of Object.keys(v).sort()) out[key] = v[key];
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
return v;
|
|
32
|
+
}, 2) + '\n';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readSettings(settingsPath) {
|
|
36
|
+
if (!existsSync(settingsPath)) return {};
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function writeManagedSettings(baseDir) {
|
|
45
|
+
const settingsPath = getSettingsPath(baseDir);
|
|
46
|
+
const ledgerPath = getLedgerPath(baseDir);
|
|
47
|
+
const settings = readSettings(settingsPath);
|
|
48
|
+
const env = settings.env || {};
|
|
49
|
+
|
|
50
|
+
const envKeys = HISTORICAL_ENV_KEYS
|
|
51
|
+
.filter((k) => Object.prototype.hasOwnProperty.call(env, k))
|
|
52
|
+
.sort();
|
|
53
|
+
|
|
54
|
+
const envValues = {};
|
|
55
|
+
for (const k of envKeys) envValues[k] = env[k];
|
|
56
|
+
|
|
57
|
+
const statusLineValue = settings.statusLine;
|
|
58
|
+
const hasExpectedStatusLine = Boolean(
|
|
59
|
+
statusLineValue
|
|
60
|
+
&& typeof statusLineValue === 'object'
|
|
61
|
+
&& statusLineValue.type === 'command'
|
|
62
|
+
&& typeof statusLineValue.command === 'string'
|
|
63
|
+
&& statusLineValue.command.includes('context_window'),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const ledger = {
|
|
67
|
+
schemaVersion: 1,
|
|
68
|
+
plugin: 'daemux-store-automator',
|
|
69
|
+
envKeys,
|
|
70
|
+
envValues,
|
|
71
|
+
statusLine: hasExpectedStatusLine
|
|
72
|
+
? { wasSet: true, content: statusLineValue }
|
|
73
|
+
: { wasSet: false, content: null },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
mkdirSync(dirname(ledgerPath), { recursive: true });
|
|
77
|
+
writeFileSync(ledgerPath, sortedStringify(ledger), 'utf8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function readManagedSettings(baseDir) {
|
|
81
|
+
const ledgerPath = getLedgerPath(baseDir);
|
|
82
|
+
if (!existsSync(ledgerPath)) return null;
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(readFileSync(ledgerPath, 'utf8'));
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/settings.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import { ensureFile, readJson, writeJson } from './utils.mjs';
|
|
3
2
|
|
|
4
3
|
const ENV_DEFAULTS = {
|
|
@@ -47,28 +46,6 @@ export function injectEnvVars(settingsPath) {
|
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
export function removeEnvVars(settingsPath) {
|
|
51
|
-
if (!existsSync(settingsPath)) return;
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const settings = readJson(settingsPath);
|
|
55
|
-
if (!settings.env) return;
|
|
56
|
-
|
|
57
|
-
for (const key of Object.keys(ENV_DEFAULTS)) {
|
|
58
|
-
delete settings.env[key];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (Object.keys(settings.env).length === 0) {
|
|
62
|
-
delete settings.env;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
writeJson(settingsPath, settings);
|
|
66
|
-
console.log('Removed env vars');
|
|
67
|
-
} catch {
|
|
68
|
-
// Silently skip if settings file is invalid
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
49
|
export function injectStatusLine(settingsPath) {
|
|
73
50
|
try {
|
|
74
51
|
ensureFile(settingsPath);
|
|
@@ -90,17 +67,3 @@ export function injectStatusLine(settingsPath) {
|
|
|
90
67
|
}
|
|
91
68
|
}
|
|
92
69
|
|
|
93
|
-
export function removeStatusLine(settingsPath) {
|
|
94
|
-
if (!existsSync(settingsPath)) return;
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const settings = readJson(settingsPath);
|
|
98
|
-
if (!settings.statusLine) return;
|
|
99
|
-
|
|
100
|
-
delete settings.statusLine;
|
|
101
|
-
writeJson(settingsPath, settings);
|
|
102
|
-
console.log('Removed statusLine configuration');
|
|
103
|
-
} catch {
|
|
104
|
-
// Silently skip if settings file is invalid
|
|
105
|
-
}
|
|
106
|
-
}
|
package/src/templates.mjs
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { existsSync, cpSync, copyFileSync, chmodSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ensureDir } from './utils.mjs';
|
|
4
|
+
import { backupIfChanging } from './backup.mjs';
|
|
5
|
+
|
|
6
|
+
const SENTINEL_BEGIN = '<!-- BEGIN daemux-store-automator -->';
|
|
7
|
+
const SENTINEL_END = '<!-- END daemux-store-automator -->';
|
|
8
|
+
const SENTINEL_BLOCK_RE = /<!-- BEGIN daemux-store-automator -->[\s\S]*?<!-- END daemux-store-automator -->/;
|
|
4
9
|
|
|
5
10
|
const FILE_COPIES = [
|
|
6
11
|
['ci.config.yaml.template', 'ci.config.yaml'],
|
|
@@ -46,18 +51,60 @@ function copyIfMissing(srcPath, destPath, label, isDirectory) {
|
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
function wrapInSentinels(body) {
|
|
55
|
+
const trimmed = body.replace(/\s+$/, '');
|
|
56
|
+
return `${SENTINEL_BEGIN}\n${trimmed}\n${SENTINEL_END}\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function looksLikeUnsentineledPluginContent(existing) {
|
|
60
|
+
if (!existing) return false;
|
|
61
|
+
const startsWithHeader = /^#\s.+-\s*Development Standards\b/m.test(existing.split('\n').slice(0, 5).join('\n'));
|
|
62
|
+
const hasFastlane = existing.includes('fastlane');
|
|
63
|
+
return startsWithHeader && hasFastlane;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function composeNewFileContent(existing, wrappedBlock) {
|
|
67
|
+
if (!existing) {
|
|
68
|
+
return wrappedBlock;
|
|
69
|
+
}
|
|
70
|
+
if (SENTINEL_BLOCK_RE.test(existing)) {
|
|
71
|
+
return existing.replace(SENTINEL_BLOCK_RE, wrappedBlock.replace(/\n$/, ''));
|
|
72
|
+
}
|
|
73
|
+
if (looksLikeUnsentineledPluginContent(existing)) {
|
|
74
|
+
return wrappedBlock;
|
|
75
|
+
}
|
|
76
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
77
|
+
return `${existing}${sep}\n${wrappedBlock}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
49
80
|
export function installClaudeMd(targetPath, packageDir, appName) {
|
|
50
81
|
const template = join(packageDir, 'templates', 'CLAUDE.md.template');
|
|
51
82
|
if (!existsSync(template)) return;
|
|
52
|
-
const
|
|
83
|
+
const targetExists = existsSync(targetPath);
|
|
84
|
+
const action = targetExists ? 'Updating' : 'Installing';
|
|
53
85
|
console.log(`${action} CLAUDE.md...`);
|
|
54
86
|
ensureDir(join(targetPath, '..'));
|
|
55
87
|
|
|
56
|
-
let
|
|
88
|
+
let body = readFileSync(template, 'utf8');
|
|
57
89
|
if (appName) {
|
|
58
|
-
|
|
90
|
+
body = body.split('{APP_NAME}').join(appName);
|
|
59
91
|
}
|
|
60
|
-
|
|
92
|
+
const wrappedBlock = wrapInSentinels(body);
|
|
93
|
+
|
|
94
|
+
const existing = targetExists ? readFileSync(targetPath, 'utf8') : '';
|
|
95
|
+
const migrating = targetExists
|
|
96
|
+
&& !SENTINEL_BLOCK_RE.test(existing)
|
|
97
|
+
&& looksLikeUnsentineledPluginContent(existing);
|
|
98
|
+
|
|
99
|
+
const newContent = composeNewFileContent(existing, wrappedBlock);
|
|
100
|
+
|
|
101
|
+
const backupResult = backupIfChanging(targetPath, newContent);
|
|
102
|
+
if (migrating) {
|
|
103
|
+
const backupPath = backupResult.path || '(no backup needed — content identical)';
|
|
104
|
+
console.log(`Migrating unsentineled plugin content to sentinel-wrapped form (backup: ${backupPath})`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
writeFileSync(targetPath, newContent, 'utf8');
|
|
61
108
|
}
|
|
62
109
|
|
|
63
110
|
export function installCiTemplates(projectDir, packageDir) {
|
package/src/uninstall.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync, rmSync, unlinkSync, readdirSync,
|
|
3
|
+
readFileSync, writeFileSync,
|
|
4
|
+
} from 'node:fs';
|
|
2
5
|
import { join } from 'node:path';
|
|
3
6
|
import { homedir } from 'node:os';
|
|
4
7
|
import { execSync } from 'node:child_process';
|
|
@@ -7,8 +10,15 @@ import {
|
|
|
7
10
|
MARKETPLACE_NAME, PLUGIN_REF,
|
|
8
11
|
readJson, writeJson,
|
|
9
12
|
} from './utils.mjs';
|
|
10
|
-
import { removeEnvVars, removeStatusLine } from './settings.mjs';
|
|
11
13
|
import { removeMcpServers } from './mcp-setup.mjs';
|
|
14
|
+
import { backupIfChanging, pruneBackups } from './backup.mjs';
|
|
15
|
+
import {
|
|
16
|
+
readManagedSettings, getLedgerPath,
|
|
17
|
+
HISTORICAL_ENV_KEYS, HISTORICAL_ENV_VALUES,
|
|
18
|
+
} from './managed-settings.mjs';
|
|
19
|
+
|
|
20
|
+
const SENTINEL_BEGIN = '<!-- BEGIN daemux-store-automator -->';
|
|
21
|
+
const SENTINEL_END = '<!-- END daemux-store-automator -->';
|
|
12
22
|
|
|
13
23
|
function runClaudeUninstall(scope) {
|
|
14
24
|
const scopeArg = scope === 'user' ? '' : ` --scope ${scope}`;
|
|
@@ -81,32 +91,156 @@ function removeGitHubWorkflow(projectDir) {
|
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
|
|
94
|
+
function spliceSentinelBlock(content) {
|
|
95
|
+
const beginIdx = content.indexOf(SENTINEL_BEGIN);
|
|
96
|
+
const endIdx = content.indexOf(SENTINEL_END);
|
|
97
|
+
if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) return null;
|
|
98
|
+
|
|
99
|
+
const before = content.slice(0, beginIdx);
|
|
100
|
+
let after = content.slice(endIdx + SENTINEL_END.length);
|
|
101
|
+
if (after.startsWith('\n')) after = after.slice(1);
|
|
102
|
+
return before.replace(/[ \t]+$/, '') + after;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function cleanClaudeMd(baseDir, scopeLabel) {
|
|
106
|
+
const claudeMdPath = join(baseDir, 'CLAUDE.md');
|
|
107
|
+
if (!existsSync(claudeMdPath)) return;
|
|
108
|
+
|
|
109
|
+
const content = readFileSync(claudeMdPath, 'utf8');
|
|
110
|
+
const spliced = spliceSentinelBlock(content);
|
|
111
|
+
|
|
112
|
+
if (spliced === null) {
|
|
113
|
+
console.log(
|
|
114
|
+
`Warning: ${scopeLabel} CLAUDE.md has no plugin sentinels; leaving untouched. ` +
|
|
115
|
+
`(If this file contains plugin content from an older version, remove the plugin section manually.)`
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
backupIfChanging(claudeMdPath, spliced);
|
|
121
|
+
if (spliced.trim() === '') {
|
|
122
|
+
rmSync(claudeMdPath);
|
|
123
|
+
console.log(`Removed ${scopeLabel} CLAUDE.md (contained only plugin content)`);
|
|
124
|
+
} else {
|
|
125
|
+
writeFileSync(claudeMdPath, spliced, 'utf8');
|
|
126
|
+
console.log(`Removed plugin section from ${scopeLabel} CLAUDE.md`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function looksLikeHistoricalPluginStatusLine(current) {
|
|
131
|
+
return Boolean(
|
|
132
|
+
current
|
|
133
|
+
&& typeof current === 'object'
|
|
134
|
+
&& current.type === 'command'
|
|
135
|
+
&& typeof current.command === 'string'
|
|
136
|
+
&& current.command.includes('context_window')
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function pruneManagedEnv(settings, envKeys, envValues) {
|
|
141
|
+
if (!settings.env) return false;
|
|
142
|
+
let modified = false;
|
|
143
|
+
for (const key of envKeys) {
|
|
144
|
+
if (!(key in settings.env)) continue;
|
|
145
|
+
const expected = envValues ? envValues[key] : undefined;
|
|
146
|
+
if (expected === undefined || settings.env[key] === expected) {
|
|
147
|
+
delete settings.env[key];
|
|
148
|
+
modified = true;
|
|
149
|
+
} else {
|
|
150
|
+
console.log(`Preserving user-modified env.${key} in settings.json`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (Object.keys(settings.env).length === 0) delete settings.env;
|
|
154
|
+
return modified;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function pruneManagedStatusLine(settings, ledgerStatusLine) {
|
|
158
|
+
if (!settings.statusLine) return false;
|
|
159
|
+
|
|
160
|
+
if (ledgerStatusLine) {
|
|
161
|
+
if (ledgerStatusLine.wasSet !== true) return false;
|
|
162
|
+
const managed = ledgerStatusLine.content;
|
|
163
|
+
if (JSON.stringify(settings.statusLine) !== JSON.stringify(managed)) {
|
|
164
|
+
console.log('Preserving user-modified statusLine in settings.json');
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
delete settings.statusLine;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!looksLikeHistoricalPluginStatusLine(settings.statusLine)) {
|
|
172
|
+
console.log('Preserving non-plugin statusLine in settings.json (no ledger, signature mismatch)');
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
delete settings.statusLine;
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function cleanSettings(baseDir, scopeLabel, ledger) {
|
|
180
|
+
const settingsPath = join(baseDir, 'settings.json');
|
|
181
|
+
if (!existsSync(settingsPath)) return;
|
|
182
|
+
|
|
183
|
+
let settings;
|
|
184
|
+
try {
|
|
185
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
186
|
+
} catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const envKeys = ledger?.envKeys ?? HISTORICAL_ENV_KEYS;
|
|
191
|
+
const envValues = ledger?.envValues ?? HISTORICAL_ENV_VALUES;
|
|
192
|
+
const envChanged = pruneManagedEnv(settings, envKeys, envValues);
|
|
193
|
+
const statusLineChanged = pruneManagedStatusLine(settings, ledger?.statusLine);
|
|
194
|
+
|
|
195
|
+
if (!envChanged && !statusLineChanged) return;
|
|
196
|
+
|
|
197
|
+
const newRaw = JSON.stringify(settings, null, 2) + '\n';
|
|
198
|
+
backupIfChanging(settingsPath, newRaw);
|
|
199
|
+
writeFileSync(settingsPath, newRaw, 'utf8');
|
|
200
|
+
console.log(`Cleaned ${scopeLabel} settings`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function removeLedger(baseDir) {
|
|
204
|
+
const ledgerPath = getLedgerPath(baseDir);
|
|
205
|
+
if (existsSync(ledgerPath)) {
|
|
206
|
+
try { rmSync(ledgerPath); } catch { /* best-effort */ }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
84
210
|
export async function runUninstall(scope) {
|
|
85
211
|
console.log(`Uninstalling Daemux Store Automator (scope: ${scope})...`);
|
|
86
212
|
|
|
87
213
|
runClaudeUninstall(scope);
|
|
88
214
|
|
|
89
|
-
if (scope === 'user') {
|
|
90
|
-
console.log('Removing marketplace...');
|
|
91
|
-
rmSync(MARKETPLACE_DIR, { recursive: true, force: true });
|
|
92
|
-
rmSync(CACHE_DIR, { recursive: true, force: true });
|
|
93
|
-
unregisterMarketplace();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
215
|
const isGlobal = scope === 'user';
|
|
97
216
|
const baseDir = isGlobal ? join(homedir(), '.claude') : join(process.cwd(), '.claude');
|
|
98
217
|
const scopeLabel = isGlobal ? 'global' : 'project';
|
|
99
218
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
219
|
+
const ledger = readManagedSettings(baseDir);
|
|
220
|
+
|
|
221
|
+
cleanClaudeMd(baseDir, scopeLabel);
|
|
222
|
+
|
|
223
|
+
if (!isGlobal) {
|
|
224
|
+
removeCiTemplates(process.cwd());
|
|
225
|
+
removeMcpServers(process.cwd());
|
|
226
|
+
}
|
|
103
227
|
|
|
104
228
|
console.log(`Cleaning ${scopeLabel} settings...`);
|
|
105
|
-
|
|
106
|
-
removeStatusLine(join(baseDir, 'settings.json'));
|
|
229
|
+
cleanSettings(baseDir, scopeLabel, ledger);
|
|
107
230
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
231
|
+
removeLedger(baseDir);
|
|
232
|
+
pruneBackups();
|
|
233
|
+
|
|
234
|
+
let doneMessage;
|
|
235
|
+
if (isGlobal) {
|
|
236
|
+
console.log('Removing marketplace...');
|
|
237
|
+
rmSync(MARKETPLACE_DIR, { recursive: true, force: true });
|
|
238
|
+
rmSync(CACHE_DIR, { recursive: true, force: true });
|
|
239
|
+
unregisterMarketplace();
|
|
240
|
+
doneMessage = '\nDone! store-automator uninstalled globally.';
|
|
241
|
+
} else {
|
|
242
|
+
doneMessage = '\nDone! store-automator uninstalled from this project.\n\nNote: Marketplace files remain under ~/.claude/plugins/marketplaces.\nRun with --global --uninstall to remove marketplace completely.';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(doneMessage);
|
|
112
246
|
}
|