@daemux/store-automator 0.10.63 → 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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "App Store & Google Play automation for Flutter apps",
8
- "version": "0.10.63"
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.63",
15
+ "version": "0.10.64",
16
16
  "keywords": [
17
17
  "flutter",
18
18
  "app-store",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemux/store-automator",
3
- "version": "0.10.63",
3
+ "version": "0.10.64",
4
4
  "description": "Full App Store & Google Play automation for Flutter apps with Claude Code agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "store-automator",
3
- "version": "0.10.63",
3
+ "version": "0.10.64",
4
4
  "description": "App Store & Google Play automation agents for Flutter app publishing",
5
5
  "author": {
6
6
  "name": "Daemux"
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');
@@ -162,13 +164,29 @@ async function withReadline(fn) {
162
164
  }
163
165
  }
164
166
 
167
+ function readIfExists(filePath) {
168
+ return existsSync(filePath) ? readFileSync(filePath, 'utf8') : null;
169
+ }
170
+
165
171
  function configureScopedSettings(baseDir, packageDir, appName, scopeLabel) {
166
172
  ensureDir(baseDir);
167
173
  installClaudeMd(join(baseDir, 'CLAUDE.md'), packageDir, appName);
168
174
  console.log(`Configuring ${scopeLabel} settings...`);
169
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
+
170
183
  injectEnvVars(settingsPath);
171
184
  injectStatusLine(settingsPath);
185
+
186
+ const postContent = readIfExists(settingsPath) ?? '';
187
+ backupSnapshot(settingsPath, preContent, postContent);
188
+
189
+ writeManagedSettings(baseDir);
172
190
  }
173
191
 
174
192
  function printGlobalNote() {
@@ -253,6 +271,8 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
253
271
  const appName = prompted.appName || (isGlobal ? 'your app' : undefined);
254
272
  configureScopedSettings(baseDir, packageDir, appName, isGlobal ? 'global' : 'project');
255
273
 
274
+ pruneBackups();
275
+
256
276
  printSummary(scope, oldVersion, newVersion);
257
277
  if (isGlobal) {
258
278
  printGlobalNote();
@@ -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 action = existsSync(targetPath) ? 'Updating' : 'Installing';
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 content = readFileSync(template, 'utf8');
88
+ let body = readFileSync(template, 'utf8');
57
89
  if (appName) {
58
- content = content.split('{APP_NAME}').join(appName);
90
+ body = body.split('{APP_NAME}').join(appName);
59
91
  }
60
- writeFileSync(targetPath, content, 'utf8');
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 { existsSync, rmSync, unlinkSync, readdirSync } from 'node:fs';
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,27 +91,134 @@ function removeGitHubWorkflow(projectDir) {
81
91
  }
82
92
  }
83
93
 
84
- export async function runUninstall(scope) {
85
- console.log(`Uninstalling Daemux Store Automator (scope: ${scope})...`);
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;
86
98
 
87
- runClaudeUninstall(scope);
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
+ }
88
104
 
89
- let doneMessage;
90
- if (scope === 'user') {
91
- console.log('Removing marketplace...');
92
- rmSync(MARKETPLACE_DIR, { recursive: true, force: true });
93
- rmSync(CACHE_DIR, { recursive: true, force: true });
94
- unregisterMarketplace();
95
- doneMessage = '\nDone! store-automator uninstalled globally.';
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)`);
96
124
  } else {
97
- 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.';
125
+ writeFileSync(claudeMdPath, spliced, 'utf8');
126
+ console.log(`Removed plugin section from ${scopeLabel} CLAUDE.md`);
98
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
+
210
+ export async function runUninstall(scope) {
211
+ console.log(`Uninstalling Daemux Store Automator (scope: ${scope})...`);
212
+
213
+ runClaudeUninstall(scope);
99
214
 
100
215
  const isGlobal = scope === 'user';
101
216
  const baseDir = isGlobal ? join(homedir(), '.claude') : join(process.cwd(), '.claude');
102
217
  const scopeLabel = isGlobal ? 'global' : 'project';
103
218
 
104
- removeFileIfExists(join(baseDir, 'CLAUDE.md'), `${scopeLabel} CLAUDE.md`);
219
+ const ledger = readManagedSettings(baseDir);
220
+
221
+ cleanClaudeMd(baseDir, scopeLabel);
105
222
 
106
223
  if (!isGlobal) {
107
224
  removeCiTemplates(process.cwd());
@@ -109,8 +226,21 @@ export async function runUninstall(scope) {
109
226
  }
110
227
 
111
228
  console.log(`Cleaning ${scopeLabel} settings...`);
112
- removeEnvVars(join(baseDir, 'settings.json'));
113
- removeStatusLine(join(baseDir, 'settings.json'));
229
+ cleanSettings(baseDir, scopeLabel, ledger);
230
+
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
+ }
114
244
 
115
245
  console.log(doneMessage);
116
246
  }