figmanage 1.2.2 → 1.2.4
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/dist/auth/cookie.d.ts +10 -0
- package/dist/auth/cookie.js +37 -1
- package/dist/operations/compound-manager.js +12 -5
- package/dist/operations/compound.d.ts +1 -11
- package/dist/operations/compound.js +31 -14
- package/dist/setup.js +6 -4
- package/package.json +1 -1
package/dist/auth/cookie.d.ts
CHANGED
|
@@ -23,10 +23,20 @@ export interface SessionInfo {
|
|
|
23
23
|
}
|
|
24
24
|
export declare function validateSession(cookieValue: string, userId: string): Promise<SessionInfo>;
|
|
25
25
|
export declare function validatePat(pat: string): Promise<string>;
|
|
26
|
+
export interface ProfileInfo {
|
|
27
|
+
profileName: string;
|
|
28
|
+
figmaEmail?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve display info for a Figma account: Figma email via API + Chrome profile name.
|
|
32
|
+
* The API call is best-effort -- falls back to userId if it fails.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveAccountInfo(account: FigmaAccount): Promise<ProfileInfo>;
|
|
26
35
|
export interface FigmaAccount {
|
|
27
36
|
userId: string;
|
|
28
37
|
cookieValue: string;
|
|
29
38
|
profile: string;
|
|
39
|
+
profilePath: string;
|
|
30
40
|
}
|
|
31
41
|
/**
|
|
32
42
|
* Extract Figma auth cookies from all Chrome profiles.
|
package/dist/auth/cookie.js
CHANGED
|
@@ -263,6 +263,42 @@ export async function validatePat(pat) {
|
|
|
263
263
|
});
|
|
264
264
|
return res.data.handle || res.data.email || 'valid';
|
|
265
265
|
}
|
|
266
|
+
function readChromeProfileName(profilePath) {
|
|
267
|
+
try {
|
|
268
|
+
const prefsPath = join(profilePath, 'Preferences');
|
|
269
|
+
if (!existsSync(prefsPath))
|
|
270
|
+
return profilePath.split(/[/\\]/).pop();
|
|
271
|
+
const prefs = JSON.parse(readFileSync(prefsPath, 'utf-8'));
|
|
272
|
+
return prefs?.profile?.name || profilePath.split(/[/\\]/).pop();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return profilePath.split(/[/\\]/).pop();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Resolve display info for a Figma account: Figma email via API + Chrome profile name.
|
|
280
|
+
* The API call is best-effort -- falls back to userId if it fails.
|
|
281
|
+
*/
|
|
282
|
+
export async function resolveAccountInfo(account) {
|
|
283
|
+
const profileName = readChromeProfileName(account.profilePath);
|
|
284
|
+
try {
|
|
285
|
+
const headers = {
|
|
286
|
+
'Cookie': `${COOKIE_NAME}=${account.cookieValue}`,
|
|
287
|
+
'X-CSRF-Bypass': 'yes',
|
|
288
|
+
'X-Figma-User-Id': account.userId,
|
|
289
|
+
};
|
|
290
|
+
const res = await axios.get('https://www.figma.com/api/user/state', {
|
|
291
|
+
headers,
|
|
292
|
+
timeout: 5000,
|
|
293
|
+
});
|
|
294
|
+
const meta = res.data?.meta || {};
|
|
295
|
+
const email = meta.email || meta.user?.email;
|
|
296
|
+
return { profileName, figmaEmail: email };
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return { profileName };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
266
302
|
/**
|
|
267
303
|
* Extract Figma auth cookies from all Chrome profiles.
|
|
268
304
|
* Returns an array of accounts found (may be empty on Windows failure).
|
|
@@ -275,7 +311,7 @@ export function extractCookies() {
|
|
|
275
311
|
try {
|
|
276
312
|
const rawCookie = extractCookieFromProfile(profilePath);
|
|
277
313
|
const { userId, cookieValue } = parseCookieValue(rawCookie);
|
|
278
|
-
accounts.push({ userId, cookieValue, profile: profilePath.split(/[/\\]/).pop() });
|
|
314
|
+
accounts.push({ userId, cookieValue, profile: profilePath.split(/[/\\]/).pop(), profilePath });
|
|
279
315
|
}
|
|
280
316
|
catch {
|
|
281
317
|
// Profile doesn't have a Figma cookie
|
|
@@ -459,14 +459,15 @@ export async function quarterlyDesignOpsReport(config, params) {
|
|
|
459
459
|
params: { plan_parent_id: orgId, plan_type: 'organization' },
|
|
460
460
|
}),
|
|
461
461
|
]);
|
|
462
|
-
// Paginate all members (cursor-based
|
|
462
|
+
// Paginate all members (cursor-based)
|
|
463
463
|
const errors = [];
|
|
464
464
|
const allMembers = [];
|
|
465
465
|
let cursor;
|
|
466
|
-
|
|
466
|
+
let membersComplete = true;
|
|
467
|
+
const maxPages = 200; // safety cap: 200 * 50 = 10,000 members
|
|
467
468
|
for (let page = 0; page < maxPages; page++) {
|
|
468
469
|
try {
|
|
469
|
-
const params = { page_size:
|
|
470
|
+
const params = { page_size: 50 };
|
|
470
471
|
if (cursor)
|
|
471
472
|
params.cursor = cursor;
|
|
472
473
|
const res = await api.get(`/api/v2/orgs/${orgId}/org_users`, { params });
|
|
@@ -476,14 +477,19 @@ export async function quarterlyDesignOpsReport(config, params) {
|
|
|
476
477
|
break;
|
|
477
478
|
allMembers.push(...batch);
|
|
478
479
|
cursor = Array.isArray(meta.cursor) ? meta.cursor[0] : meta.cursor;
|
|
479
|
-
if (!cursor || batch.length <
|
|
480
|
+
if (!cursor || batch.length < 50)
|
|
480
481
|
break;
|
|
481
482
|
}
|
|
482
483
|
catch (e) {
|
|
483
|
-
|
|
484
|
+
membersComplete = false;
|
|
485
|
+
errors.push(`members: pagination stopped at page ${page + 1} of ${maxPages}, fetched ${allMembers.length} members (${e.response?.status || e.message})`);
|
|
484
486
|
break;
|
|
485
487
|
}
|
|
486
488
|
}
|
|
489
|
+
if (cursor && allMembers.length >= maxPages * 50) {
|
|
490
|
+
membersComplete = false;
|
|
491
|
+
errors.push(`members: hit ${maxPages}-page safety cap at ${allMembers.length} members, org may have more`);
|
|
492
|
+
}
|
|
487
493
|
// Process teams
|
|
488
494
|
const teamsRaw = teamsResult.status === 'fulfilled'
|
|
489
495
|
? (teamsResult.value.data?.meta || teamsResult.value.data || [])
|
|
@@ -612,6 +618,7 @@ export async function quarterlyDesignOpsReport(config, params) {
|
|
|
612
618
|
total_teams: teams.length,
|
|
613
619
|
total_members: allMembers.length,
|
|
614
620
|
total_paid_seats: totalPaid,
|
|
621
|
+
members_complete: membersComplete,
|
|
615
622
|
},
|
|
616
623
|
seat_utilization: {
|
|
617
624
|
active_paid: activePaid,
|
|
@@ -47,17 +47,7 @@ export declare function seatOptimization(config: AuthConfig, params: {
|
|
|
47
47
|
org_id?: string;
|
|
48
48
|
days_inactive: number;
|
|
49
49
|
include_cost: boolean;
|
|
50
|
-
}): Promise<
|
|
51
|
-
summary: {
|
|
52
|
-
total_paid: number;
|
|
53
|
-
inactive_paid: number;
|
|
54
|
-
monthly_waste_cents: any;
|
|
55
|
-
annual_savings_cents: number;
|
|
56
|
-
};
|
|
57
|
-
seat_breakdown: any;
|
|
58
|
-
inactive_users: any[];
|
|
59
|
-
recommendations: string[];
|
|
60
|
-
}>;
|
|
50
|
+
}): Promise<Record<string, any>>;
|
|
61
51
|
export declare function permissionAudit(config: AuthConfig, params: {
|
|
62
52
|
scope_type: 'project' | 'team';
|
|
63
53
|
scope_id: string;
|
|
@@ -242,23 +242,36 @@ export async function seatOptimization(config, params) {
|
|
|
242
242
|
const api = internalClient(config);
|
|
243
243
|
const cutoff = Date.now() - days_inactive * 86400000;
|
|
244
244
|
const paidKeys = new Set(['expert', 'developer', 'collaborator']);
|
|
245
|
-
// Paginate org members (cursor-based
|
|
245
|
+
// Paginate org members (cursor-based)
|
|
246
246
|
const allMembers = [];
|
|
247
|
-
const
|
|
247
|
+
const warnings = [];
|
|
248
|
+
let membersComplete = true;
|
|
249
|
+
const MAX_PAGES = 200; // safety cap: 200 * 50 = 10,000 members
|
|
248
250
|
let cursor;
|
|
249
251
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
252
|
+
try {
|
|
253
|
+
const params = { page_size: 50 };
|
|
254
|
+
if (cursor)
|
|
255
|
+
params.cursor = cursor;
|
|
256
|
+
const res = await api.get(`/api/v2/orgs/${orgId}/org_users`, { params });
|
|
257
|
+
const meta = res.data?.meta || {};
|
|
258
|
+
const members = meta.users || [];
|
|
259
|
+
if (!Array.isArray(members) || members.length === 0)
|
|
260
|
+
break;
|
|
261
|
+
allMembers.push(...members);
|
|
262
|
+
cursor = Array.isArray(meta.cursor) ? meta.cursor[0] : meta.cursor;
|
|
263
|
+
if (!cursor || members.length < 50)
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
membersComplete = false;
|
|
268
|
+
warnings.push(`members: pagination stopped at page ${page + 1}, fetched ${allMembers.length} members (${e.response?.status || e.message})`);
|
|
261
269
|
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (cursor && allMembers.length >= MAX_PAGES * 50) {
|
|
273
|
+
membersComplete = false;
|
|
274
|
+
warnings.push(`members: hit ${MAX_PAGES}-page safety cap at ${allMembers.length} members, org may have more`);
|
|
262
275
|
}
|
|
263
276
|
// Fetch seat breakdown and optionally contract rates in parallel
|
|
264
277
|
const parallelCalls = [
|
|
@@ -317,17 +330,21 @@ export async function seatOptimization(config, params) {
|
|
|
317
330
|
if (monthlyWasteCents > 0) {
|
|
318
331
|
recommendations.push(`Potential monthly savings: $${(monthlyWasteCents / 100).toFixed(2)} ($${((monthlyWasteCents * 12) / 100).toFixed(2)}/yr).`);
|
|
319
332
|
}
|
|
320
|
-
|
|
333
|
+
const result = {
|
|
321
334
|
summary: {
|
|
322
335
|
total_paid: totalPaid,
|
|
323
336
|
inactive_paid: inactiveUsers.length,
|
|
324
337
|
monthly_waste_cents: monthlyWasteCents,
|
|
325
338
|
annual_savings_cents: monthlyWasteCents * 12,
|
|
339
|
+
members_complete: membersComplete,
|
|
326
340
|
},
|
|
327
341
|
seat_breakdown: seats,
|
|
328
342
|
inactive_users: inactiveUsers,
|
|
329
343
|
recommendations,
|
|
330
344
|
};
|
|
345
|
+
if (warnings.length > 0)
|
|
346
|
+
result.warnings = warnings;
|
|
347
|
+
return result;
|
|
331
348
|
}
|
|
332
349
|
// -- permission_audit --
|
|
333
350
|
export async function permissionAudit(config, params) {
|
package/dist/setup.js
CHANGED
|
@@ -3,7 +3,7 @@ import { execFileSync } from 'child_process';
|
|
|
3
3
|
import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { homedir, platform } from 'os';
|
|
6
|
-
import { extractCookies, validateSession, validatePat } from './auth/cookie.js';
|
|
6
|
+
import { extractCookies, validateSession, validatePat, resolveAccountInfo } from './auth/cookie.js';
|
|
7
7
|
// --- MCP client detection and registration ---
|
|
8
8
|
function claudeCliAvailable() {
|
|
9
9
|
try {
|
|
@@ -187,10 +187,12 @@ async function setup() {
|
|
|
187
187
|
// If multiple accounts, let user pick
|
|
188
188
|
let selected = accounts[0];
|
|
189
189
|
if (accounts.length > 1) {
|
|
190
|
-
console.log(`\n Found ${accounts.length} Figma accounts
|
|
190
|
+
console.log(`\n Found ${accounts.length} Figma accounts. Identifying...\n`);
|
|
191
|
+
const infos = await Promise.all(accounts.map(a => resolveAccountInfo(a)));
|
|
191
192
|
for (let i = 0; i < accounts.length; i++) {
|
|
192
|
-
const
|
|
193
|
-
|
|
193
|
+
const info = infos[i];
|
|
194
|
+
const label = info.figmaEmail || `User ${accounts[i].userId}`;
|
|
195
|
+
console.log(` [${i + 1}] ${label} (Chrome: ${info.profileName})`);
|
|
194
196
|
}
|
|
195
197
|
const { createInterface } = await import('readline');
|
|
196
198
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
package/package.json
CHANGED