skills-script 1.1.1 → 1.2.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/README.md CHANGED
@@ -1,57 +1,54 @@
1
1
  # skills-script
2
2
 
3
- Interactive skills toolkit CLI.
3
+ 交互式技能工具 CLI
4
4
 
5
- ## Run
5
+ ## 运行
6
6
 
7
7
  ```bash
8
8
  npx skills-script
9
9
  ```
10
10
 
11
- Flow:
12
-
13
- 1. Select tool (currently: `skills-sync`).
14
- 2. Select platforms with Space (multi-select).
15
- 3. Press Enter to confirm.
16
- 4. Optional: input custom platform path tokens (for example: `.aaa`).
17
-
18
- ## Platform Selection
19
-
20
- Built-in options:
21
-
22
- - `all`
23
- - `claude`
24
- - `codex`
25
- - `gemini`
26
-
27
- Built-in targets are under command invocation directory:
28
-
29
- - `claude` => `<cwd>/.claude/skills`
30
- - `codex` => `<cwd>/.codex/skills`
31
- - `gemini` => `<cwd>/.gemini/skills`
32
-
33
- Custom token rules:
34
-
35
- - `.aaa` => `<cwd>/.aaa/skills`
36
- - absolute/relative path => used directly as target directory
37
-
38
- ## Source Skills Directory
39
-
40
- The source is fixed to:
11
+ ## 脚本整体运作流程
12
+
13
+ 1. 启动入口:执行 `bin/skill-script.js`,进入 `src/cli.js`。
14
+ 2. 解析命令参数:
15
+ - 无参数:进入交互模式。
16
+ - `-h/--help/help`:输出帮助信息并退出。
17
+ - `-v/--version/version`:输出版本并退出。
18
+ - `sync ...`:进入非交互同步流程。
19
+ 3. 解析工作目录:优先使用调用上下文目录(用于避免 npx 临时目录干扰)。
20
+ 4. 执行对应流程:
21
+ - 交互模式:先选工具,再进入该工具的交互步骤。
22
+ - 非交互模式:直接按命令参数执行目标工具。
23
+ 5. 返回退出码:
24
+ - 成功返回 `0`。
25
+ - 参数错误或运行失败返回非 `0`。
26
+
27
+ ## 交互模式流程
28
+
29
+ 1. 工具选择:
30
+ - `skills-sync`
31
+ - `claude-settings-switch`
32
+ - `claude-auth-switch`
33
+ 2. 进入对应工具流程并执行。
34
+ 3. `claude-settings-switch` 固定操作 `~/.claude`(例如 `/Users/youzi/.claude`)。
35
+ 4. `claude-auth-switch` 管理官方登录授权账号的备份与切换(Keychain + `~/.claude.json`),账号档案隔离存放在 `~/.claude/auth-accounts/`。
36
+
37
+ ## 非交互模式
41
38
 
42
39
  ```bash
43
- <command invocation directory>/.agents/skills
40
+ npx skills-script sync claude,codex,.aaa
44
41
  ```
45
42
 
46
- When running through npm/npx, the tool prefers `INIT_CWD` to avoid temp-cache working directory issues.
43
+ ## 工具实现文档
47
44
 
48
- ## Non-Interactive Mode
45
+ 每个工具的实现细节单独记录:
49
46
 
50
- ```bash
51
- npx skills-script sync claude,codex,.aaa
52
- ```
47
+ - `docs/tools/skills-sync.md`
48
+ - `docs/tools/claude-settings-switch.md`
49
+ - `docs/tools/claude-auth-switch.md`
53
50
 
54
- ## Publish
51
+ ## 发布
55
52
 
56
53
  ```bash
57
54
  cd /Users/youzi/Desktop/__yz/skills-script
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-script",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Interactive skills toolkit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { multiSelect, promptInput, selectOne } from '../lib/interactive.js';
3
3
  import { resolveTargetDirs, runSyncByTargets } from './sync.js';
4
+ import { runClaudeSettingSwitch } from './switch-setting.js';
5
+ import { runClaudeAuthSwitch } from './switch-auth.js';
4
6
 
5
7
  function parseCustomInput(input) {
6
8
  if (!input) return [];
@@ -13,9 +15,21 @@ function parseCustomInput(input) {
13
15
  export async function runInteractive(cwd = process.cwd()) {
14
16
  const tool = await selectOne({
15
17
  title: 'Select a tool',
16
- options: [{ label: 'skills-sync', value: 'skills-sync' }]
18
+ options: [
19
+ { label: 'skills-sync', value: 'skills-sync' },
20
+ { label: 'claude-settings-switch', value: 'claude-settings-switch' },
21
+ { label: 'claude-auth-switch', value: 'claude-auth-switch' }
22
+ ]
17
23
  });
18
24
 
25
+ if (tool === 'claude-settings-switch') {
26
+ return runClaudeSettingSwitch();
27
+ }
28
+
29
+ if (tool === 'claude-auth-switch') {
30
+ return runClaudeAuthSwitch();
31
+ }
32
+
19
33
  if (tool !== 'skills-sync') {
20
34
  console.error('[ERROR] Unsupported tool selected.');
21
35
  return 1;
@@ -0,0 +1,154 @@
1
+ import { selectOne } from '../lib/interactive.js';
2
+ import {
3
+ applyAccountState,
4
+ deleteProfile,
5
+ getCurrentAccount,
6
+ listProfiles,
7
+ loadProfile,
8
+ saveProfile,
9
+ writeClaudeJson,
10
+ writeCredentials
11
+ } from '../lib/claude-auth.js';
12
+
13
+ function formatProfileLabel(profile, currentSlug) {
14
+ const parts = [profile.email];
15
+ if (profile.slug === currentSlug) parts.push('(current)');
16
+ if (profile.corrupted) parts.push('(corrupted)');
17
+ if (profile.savedAt) parts.push(`saved ${profile.savedAt}`);
18
+ return parts.join(' ');
19
+ }
20
+
21
+ async function backupCurrentAccount(current) {
22
+ const filePath = await saveProfile({
23
+ slug: current.slug,
24
+ email: current.email,
25
+ credentials: current.credentials,
26
+ claudeJson: current.claudeJson
27
+ });
28
+ console.log(`[OK] backup: ${filePath}`);
29
+ }
30
+
31
+ async function runSaveCurrent(current) {
32
+ if (!current.loggedIn) {
33
+ console.error('[ERROR] No active login found (credentials or oauthAccount missing).');
34
+ return 1;
35
+ }
36
+ await backupCurrentAccount(current);
37
+ return 0;
38
+ }
39
+
40
+ async function runSwitch(current) {
41
+ const profiles = (await listProfiles()).filter((p) => !p.corrupted);
42
+ const candidates = profiles.filter((p) => p.slug !== current.slug);
43
+
44
+ if (!candidates.length) {
45
+ console.error('[ERROR] No other saved account to switch to.');
46
+ console.error(' Save the current account first, then log in with another account and run this tool again.');
47
+ return 1;
48
+ }
49
+
50
+ const targetSlug = await selectOne({
51
+ title: `Switch Claude auth account (current: ${current.email || 'not logged in'})`,
52
+ options: candidates.map((p) => ({
53
+ label: formatProfileLabel(p, current.slug),
54
+ value: p.slug
55
+ }))
56
+ });
57
+
58
+ const target = await loadProfile(targetSlug);
59
+
60
+ if (current.loggedIn) {
61
+ await backupCurrentAccount(current);
62
+ }
63
+
64
+ try {
65
+ await writeCredentials(target.credentials);
66
+ } catch (error) {
67
+ console.error(`[ERROR] Failed to write credentials: ${error?.message || error}`);
68
+ return 1;
69
+ }
70
+
71
+ try {
72
+ const nextClaudeJson = applyAccountState(current.claudeJson, target.accountState || {});
73
+ await writeClaudeJson(nextClaudeJson);
74
+ } catch (error) {
75
+ // Roll credentials back so keychain and ~/.claude.json stay consistent.
76
+ if (current.credentials) {
77
+ try {
78
+ await writeCredentials(current.credentials);
79
+ } catch {
80
+ console.error('[ERROR] Rollback of credentials also failed. Re-login may be required.');
81
+ }
82
+ }
83
+ console.error(`[ERROR] Failed to update ~/.claude.json: ${error?.message || error}`);
84
+ return 1;
85
+ }
86
+
87
+ console.log(`[OK] active account: ${target.email || targetSlug}`);
88
+ console.log('[WARN] Restart running Claude Code sessions to pick up the new account.');
89
+ return 0;
90
+ }
91
+
92
+ async function runDelete(current) {
93
+ const profiles = await listProfiles();
94
+ if (!profiles.length) {
95
+ console.error('[ERROR] No saved account profile found.');
96
+ return 1;
97
+ }
98
+
99
+ const targetSlug = await selectOne({
100
+ title: 'Delete a saved auth account profile',
101
+ options: profiles.map((p) => ({
102
+ label: formatProfileLabel(p, current.slug),
103
+ value: p.slug
104
+ }))
105
+ });
106
+
107
+ const confirmed = await selectOne({
108
+ title: `Delete profile "${targetSlug}"? The saved tokens will be removed permanently.`,
109
+ options: [
110
+ { label: 'No, keep it', value: false },
111
+ { label: 'Yes, delete', value: true }
112
+ ]
113
+ });
114
+
115
+ if (!confirmed) {
116
+ console.log('[INFO] Canceled.');
117
+ return 0;
118
+ }
119
+
120
+ const filePath = await deleteProfile(targetSlug);
121
+ console.log(`[OK] deleted: ${filePath}`);
122
+ return 0;
123
+ }
124
+
125
+ export async function runClaudeAuthSwitch() {
126
+ let current;
127
+ try {
128
+ current = await getCurrentAccount();
129
+ } catch (error) {
130
+ console.error(`[ERROR] Failed to read current auth state: ${error?.message || error}`);
131
+ return 1;
132
+ }
133
+
134
+ const action = await selectOne({
135
+ title: `Claude auth account (current: ${current.email || 'not logged in'})`,
136
+ options: [
137
+ { label: 'Switch account', value: 'switch' },
138
+ { label: 'Backup current account', value: 'save' },
139
+ { label: 'Delete saved profile', value: 'delete' }
140
+ ]
141
+ });
142
+
143
+ try {
144
+ if (action === 'switch') return await runSwitch(current);
145
+ if (action === 'save') return await runSaveCurrent(current);
146
+ if (action === 'delete') return await runDelete(current);
147
+ } catch (error) {
148
+ console.error(`[ERROR] ${error?.message || error}`);
149
+ return 1;
150
+ }
151
+
152
+ console.error('[ERROR] Unsupported action.');
153
+ return 1;
154
+ }
@@ -0,0 +1,148 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { selectOne } from '../lib/interactive.js';
5
+
6
+ function findAnthropicBaseUrlValue(input) {
7
+ if (!input || typeof input !== 'object') return null;
8
+
9
+ if (
10
+ Object.prototype.hasOwnProperty.call(input, 'ANTHROPIC_BASE_URL') &&
11
+ typeof input.ANTHROPIC_BASE_URL === 'string'
12
+ ) {
13
+ return input.ANTHROPIC_BASE_URL;
14
+ }
15
+
16
+ for (const value of Object.values(input)) {
17
+ if (!value || typeof value !== 'object') continue;
18
+ const nested = findAnthropicBaseUrlValue(value);
19
+ if (nested) return nested;
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ function normalizeBaseUrlName(value) {
26
+ if (typeof value !== 'string') return null;
27
+ const raw = value.trim();
28
+ if (!raw) return null;
29
+
30
+ let host = raw;
31
+ try {
32
+ const parsed = new URL(raw.includes('://') ? raw : `https://${raw}`);
33
+ host = parsed.hostname || raw;
34
+ } catch {
35
+ host = raw.replace(/^[a-zA-Z]+:\/\//, '').split('/')[0] || raw;
36
+ }
37
+
38
+ const cleaned = host
39
+ .toLowerCase()
40
+ .replace(/[^a-z0-9.-]+/g, '-')
41
+ .replace(/-+/g, '-')
42
+ .replace(/^-+|-+$/g, '');
43
+
44
+ return cleaned || null;
45
+ }
46
+
47
+ async function listSettingsFiles(claudeDir) {
48
+ const entries = await fs.readdir(claudeDir, { withFileTypes: true });
49
+ return entries
50
+ .filter((entry) => entry.isFile() && /^settings.*\.json$/i.test(entry.name))
51
+ .map((entry) => entry.name)
52
+ .sort((a, b) => a.localeCompare(b));
53
+ }
54
+
55
+ async function getUniqueBackupPath(claudeDir, preferredName) {
56
+ const preferredPath = path.join(claudeDir, preferredName);
57
+
58
+ try {
59
+ await fs.access(preferredPath);
60
+ } catch {
61
+ return preferredPath;
62
+ }
63
+
64
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
65
+ const fallbackName = preferredName.replace(/\.json$/i, `.${stamp}.json`);
66
+ return path.join(claudeDir, fallbackName);
67
+ }
68
+
69
+ export async function runClaudeSettingSwitch() {
70
+ const claudeDir = path.join(os.homedir(), '.claude');
71
+ const activeSettingsPath = path.join(claudeDir, 'settings.json');
72
+
73
+ let claudeStat;
74
+ try {
75
+ claudeStat = await fs.stat(claudeDir);
76
+ } catch {
77
+ console.error(`[ERROR] Claude directory not found: ${claudeDir}`);
78
+ return 1;
79
+ }
80
+
81
+ if (!claudeStat.isDirectory()) {
82
+ console.error(`[ERROR] Not a directory: ${claudeDir}`);
83
+ return 1;
84
+ }
85
+
86
+ let candidates = [];
87
+ try {
88
+ candidates = await listSettingsFiles(claudeDir);
89
+ } catch (error) {
90
+ console.error(`[ERROR] Failed to read settings files: ${error?.message || error}`);
91
+ return 1;
92
+ }
93
+
94
+ if (!candidates.length) {
95
+ console.error(`[ERROR] No settings*.json found in ${claudeDir}`);
96
+ return 1;
97
+ }
98
+
99
+ const selected = await selectOne({
100
+ title: 'Select target Claude settings file',
101
+ options: candidates.map((name) => ({ label: name, value: name }))
102
+ });
103
+
104
+ if (selected === 'settings.json') {
105
+ console.log('[INFO] settings.json is already selected. Nothing to switch.');
106
+ return 0;
107
+ }
108
+
109
+ let activeJson;
110
+ try {
111
+ const raw = await fs.readFile(activeSettingsPath, 'utf8');
112
+ activeJson = JSON.parse(raw);
113
+ } catch (error) {
114
+ console.error(`[ERROR] Failed to read ${activeSettingsPath}: ${error?.message || error}`);
115
+ return 1;
116
+ }
117
+
118
+ const baseUrlValue = findAnthropicBaseUrlValue(activeJson);
119
+ // Without ANTHROPIC_BASE_URL the active settings use the official login,
120
+ // so back it up as the "auth" variant.
121
+ const baseUrlName = normalizeBaseUrlName(baseUrlValue) || 'auth';
122
+
123
+ const backupPath = await getUniqueBackupPath(claudeDir, `settings.${baseUrlName}.json`);
124
+ const selectedPath = path.join(claudeDir, selected);
125
+
126
+ try {
127
+ await fs.rename(activeSettingsPath, backupPath);
128
+ } catch (error) {
129
+ console.error(`[ERROR] Failed to backup current settings: ${error?.message || error}`);
130
+ return 1;
131
+ }
132
+
133
+ try {
134
+ await fs.rename(selectedPath, activeSettingsPath);
135
+ } catch (error) {
136
+ try {
137
+ await fs.rename(backupPath, activeSettingsPath);
138
+ } catch {
139
+ // If rollback also fails, preserve original failure message below.
140
+ }
141
+ console.error(`[ERROR] Failed to activate selected settings: ${error?.message || error}`);
142
+ return 1;
143
+ }
144
+
145
+ console.log(`[OK] backup: ${path.basename(backupPath)}`);
146
+ console.log(`[OK] active: settings.json <= ${selected}`);
147
+ return 0;
148
+ }
@@ -0,0 +1,265 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ const KEYCHAIN_SERVICE = 'Claude Code-credentials';
8
+
9
+ // Fields in ~/.claude.json that belong to the logged-in account. They are
10
+ // swapped together with the credentials so quota/subscription/model-access
11
+ // state never leaks from one account into another.
12
+ export const ACCOUNT_SCOPED_KEYS = [
13
+ 'oauthAccount',
14
+ 'userID',
15
+ 'hasAvailableSubscription',
16
+ 'subscriptionNoticeCount',
17
+ 'cachedExtraUsageDisabledReason',
18
+ 'modelAccessCache',
19
+ 's1mAccessCache',
20
+ 'passesEligibilityCache',
21
+ 'orgModelDefaultCache',
22
+ 'additionalModelCostsCache',
23
+ 'additionalModelOptionsCache',
24
+ 'clientDataCacheSlots'
25
+ ];
26
+
27
+ export function getClaudeDir() {
28
+ return path.join(os.homedir(), '.claude');
29
+ }
30
+
31
+ export function getAuthStoreDir() {
32
+ return path.join(getClaudeDir(), 'auth-accounts');
33
+ }
34
+
35
+ export function getClaudeJsonPath() {
36
+ return path.join(os.homedir(), '.claude.json');
37
+ }
38
+
39
+ function isDarwin() {
40
+ return process.platform === 'darwin';
41
+ }
42
+
43
+ function getCredentialsFilePath() {
44
+ return path.join(getClaudeDir(), '.credentials.json');
45
+ }
46
+
47
+ function runSecurity(args, { stdinText } = {}) {
48
+ return new Promise((resolve) => {
49
+ const child = spawn('security', args, {
50
+ stdio: [stdinText === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe']
51
+ });
52
+
53
+ let stdout = '';
54
+ let stderr = '';
55
+ child.stdout.on('data', (chunk) => {
56
+ stdout += chunk;
57
+ });
58
+ child.stderr.on('data', (chunk) => {
59
+ stderr += chunk;
60
+ });
61
+ child.on('error', (error) => {
62
+ resolve({ code: -1, stdout, stderr: String(error?.message || error) });
63
+ });
64
+ child.on('close', (code) => {
65
+ resolve({ code: code ?? -1, stdout, stderr });
66
+ });
67
+
68
+ if (stdinText !== undefined) {
69
+ child.stdin.end(stdinText);
70
+ }
71
+ });
72
+ }
73
+
74
+ function quoteForSecurityShell(value) {
75
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
76
+ }
77
+
78
+ async function readKeychainCredentials() {
79
+ const result = await runSecurity([
80
+ 'find-generic-password',
81
+ '-s',
82
+ KEYCHAIN_SERVICE,
83
+ '-w'
84
+ ]);
85
+ if (result.code !== 0) return null;
86
+ const raw = result.stdout.replace(/\n$/, '');
87
+ return raw || null;
88
+ }
89
+
90
+ // Writes through `security -i` (commands over stdin) so the token never
91
+ // appears in the process argument list.
92
+ async function writeKeychainCredentials(raw) {
93
+ const command = [
94
+ 'add-generic-password',
95
+ '-U',
96
+ '-s',
97
+ quoteForSecurityShell(KEYCHAIN_SERVICE),
98
+ '-a',
99
+ quoteForSecurityShell(os.userInfo().username),
100
+ '-w',
101
+ quoteForSecurityShell(raw)
102
+ ].join(' ');
103
+
104
+ const result = await runSecurity(['-i'], { stdinText: `${command}\n` });
105
+ if (result.code !== 0) {
106
+ throw new Error(`Keychain write failed: ${result.stderr.trim() || `exit ${result.code}`}`);
107
+ }
108
+ }
109
+
110
+ export async function readCredentials() {
111
+ if (isDarwin()) {
112
+ return readKeychainCredentials();
113
+ }
114
+
115
+ try {
116
+ return await fs.readFile(getCredentialsFilePath(), 'utf8');
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ export async function writeCredentials(raw) {
123
+ if (isDarwin()) {
124
+ await writeKeychainCredentials(raw);
125
+ return;
126
+ }
127
+
128
+ await writeFileAtomic(getCredentialsFilePath(), raw, 0o600);
129
+ }
130
+
131
+ async function writeFileAtomic(filePath, content, mode) {
132
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
133
+ await fs.writeFile(tmpPath, content, { mode });
134
+ await fs.rename(tmpPath, filePath);
135
+ }
136
+
137
+ export async function readClaudeJson() {
138
+ const raw = await fs.readFile(getClaudeJsonPath(), 'utf8');
139
+ return JSON.parse(raw);
140
+ }
141
+
142
+ export async function writeClaudeJson(json) {
143
+ await writeFileAtomic(getClaudeJsonPath(), JSON.stringify(json, null, 2), 0o600);
144
+ }
145
+
146
+ export function extractAccountState(claudeJson) {
147
+ const state = {};
148
+ for (const key of ACCOUNT_SCOPED_KEYS) {
149
+ if (Object.prototype.hasOwnProperty.call(claudeJson, key)) {
150
+ state[key] = claudeJson[key];
151
+ }
152
+ }
153
+ return state;
154
+ }
155
+
156
+ export function applyAccountState(claudeJson, state) {
157
+ const next = { ...claudeJson };
158
+ for (const key of ACCOUNT_SCOPED_KEYS) {
159
+ if (Object.prototype.hasOwnProperty.call(state, key)) {
160
+ next[key] = state[key];
161
+ } else {
162
+ delete next[key];
163
+ }
164
+ }
165
+ return next;
166
+ }
167
+
168
+ export function normalizeAccountSlug(email) {
169
+ if (typeof email !== 'string') return null;
170
+ const cleaned = email
171
+ .trim()
172
+ .toLowerCase()
173
+ .replace(/[^a-z0-9.-]+/g, '-')
174
+ .replace(/-+/g, '-')
175
+ .replace(/^-+|-+$/g, '');
176
+ return cleaned || null;
177
+ }
178
+
179
+ export function profileFileName(slug) {
180
+ return `auth.${slug}.json`;
181
+ }
182
+
183
+ async function ensureStoreDir() {
184
+ const dir = getAuthStoreDir();
185
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
186
+ // mkdir does not change the mode of a pre-existing directory.
187
+ await fs.chmod(dir, 0o700);
188
+ return dir;
189
+ }
190
+
191
+ export async function getCurrentAccount() {
192
+ const [credentials, claudeJson] = await Promise.all([
193
+ readCredentials(),
194
+ readClaudeJson()
195
+ ]);
196
+
197
+ const email = claudeJson?.oauthAccount?.emailAddress || null;
198
+ return {
199
+ credentials,
200
+ claudeJson,
201
+ email,
202
+ slug: normalizeAccountSlug(email),
203
+ loggedIn: Boolean(credentials && email)
204
+ };
205
+ }
206
+
207
+ export async function saveProfile({ slug, email, credentials, claudeJson }) {
208
+ const dir = await ensureStoreDir();
209
+ const filePath = path.join(dir, profileFileName(slug));
210
+ const profile = {
211
+ version: 1,
212
+ email,
213
+ savedAt: new Date().toISOString(),
214
+ credentials,
215
+ accountState: extractAccountState(claudeJson)
216
+ };
217
+ await writeFileAtomic(filePath, JSON.stringify(profile, null, 2), 0o600);
218
+ await fs.chmod(filePath, 0o600);
219
+ return filePath;
220
+ }
221
+
222
+ export async function listProfiles() {
223
+ let entries;
224
+ try {
225
+ entries = await fs.readdir(getAuthStoreDir(), { withFileTypes: true });
226
+ } catch {
227
+ return [];
228
+ }
229
+
230
+ const profiles = [];
231
+ for (const entry of entries) {
232
+ const match = entry.isFile() && entry.name.match(/^auth\.(.+)\.json$/);
233
+ if (!match) continue;
234
+
235
+ const filePath = path.join(getAuthStoreDir(), entry.name);
236
+ try {
237
+ const profile = JSON.parse(await fs.readFile(filePath, 'utf8'));
238
+ profiles.push({
239
+ slug: match[1],
240
+ email: profile.email || match[1],
241
+ savedAt: profile.savedAt || null,
242
+ filePath
243
+ });
244
+ } catch {
245
+ profiles.push({ slug: match[1], email: match[1], savedAt: null, filePath, corrupted: true });
246
+ }
247
+ }
248
+
249
+ return profiles.sort((a, b) => a.slug.localeCompare(b.slug));
250
+ }
251
+
252
+ export async function loadProfile(slug) {
253
+ const filePath = path.join(getAuthStoreDir(), profileFileName(slug));
254
+ const profile = JSON.parse(await fs.readFile(filePath, 'utf8'));
255
+ if (!profile || typeof profile !== 'object' || !profile.credentials) {
256
+ throw new Error(`Profile is missing credentials: ${filePath}`);
257
+ }
258
+ return profile;
259
+ }
260
+
261
+ export async function deleteProfile(slug) {
262
+ const filePath = path.join(getAuthStoreDir(), profileFileName(slug));
263
+ await fs.rm(filePath);
264
+ return filePath;
265
+ }