figmanage 0.3.0 → 1.0.1
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 +172 -254
- package/dist/auth/client.d.ts +16 -0
- package/dist/auth/client.js +44 -1
- package/dist/auth/cookie.d.ts +37 -0
- package/dist/auth/cookie.js +286 -0
- package/dist/cli/commands.d.ts +47 -0
- package/dist/cli/commands.js +1204 -0
- package/dist/cli/format.d.ts +13 -0
- package/dist/cli/format.js +21 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +115 -0
- package/dist/cli/login.d.ts +7 -0
- package/dist/cli/login.js +135 -0
- package/dist/cli/whoami.d.ts +2 -0
- package/dist/cli/whoami.js +68 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +83 -0
- package/dist/index.d.ts +1 -18
- package/dist/index.js +25 -97
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.js +100 -0
- package/package.json +2 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Check if stdout is a TTY (interactive terminal) */
|
|
2
|
+
export declare function isTTY(): boolean;
|
|
3
|
+
/** Format output: JSON if piped or --json flag, human-readable if TTY */
|
|
4
|
+
export declare function formatOutput(data: unknown, options: {
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}): string;
|
|
7
|
+
/** Print formatted output to stdout */
|
|
8
|
+
export declare function output(data: unknown, options?: {
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}): void;
|
|
11
|
+
/** Print error message to stderr */
|
|
12
|
+
export declare function error(message: string): void;
|
|
13
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Check if stdout is a TTY (interactive terminal) */
|
|
2
|
+
export function isTTY() {
|
|
3
|
+
return process.stdout.isTTY === true;
|
|
4
|
+
}
|
|
5
|
+
/** Format output: JSON if piped or --json flag, human-readable if TTY */
|
|
6
|
+
export function formatOutput(data, options) {
|
|
7
|
+
if (options.json || !isTTY()) {
|
|
8
|
+
return JSON.stringify(data, null, 2);
|
|
9
|
+
}
|
|
10
|
+
// Table formatting will be added later; fall back to JSON for now
|
|
11
|
+
return JSON.stringify(data, null, 2);
|
|
12
|
+
}
|
|
13
|
+
/** Print formatted output to stdout */
|
|
14
|
+
export function output(data, options = {}) {
|
|
15
|
+
console.log(formatOutput(data, options));
|
|
16
|
+
}
|
|
17
|
+
/** Print error message to stderr */
|
|
18
|
+
export function error(message) {
|
|
19
|
+
console.error(`error: ${message}`);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export function registerCliCommands(program) {
|
|
2
|
+
// Auth commands -- handlers live in cli/login.ts (built by another agent)
|
|
3
|
+
program
|
|
4
|
+
.command('login')
|
|
5
|
+
.description('Authenticate with Figma')
|
|
6
|
+
.option('--refresh', 'Refresh cookie only')
|
|
7
|
+
.option('--pat-only', 'PAT authentication only')
|
|
8
|
+
.action(async (options) => {
|
|
9
|
+
const { handleLogin } = await import('./login.js');
|
|
10
|
+
await handleLogin(options);
|
|
11
|
+
});
|
|
12
|
+
program
|
|
13
|
+
.command('whoami')
|
|
14
|
+
.description('Show current authentication status')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
const { handleWhoami } = await import('./whoami.js');
|
|
17
|
+
await handleWhoami();
|
|
18
|
+
});
|
|
19
|
+
program
|
|
20
|
+
.command('logout')
|
|
21
|
+
.description('Clear stored credentials')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
const { handleLogout } = await import('./login.js');
|
|
24
|
+
await handleLogout();
|
|
25
|
+
});
|
|
26
|
+
// Workspace management commands
|
|
27
|
+
program
|
|
28
|
+
.command('seat-optimization')
|
|
29
|
+
.description('Identify inactive paid seats and calculate savings')
|
|
30
|
+
.option('--days-inactive <days>', 'Days threshold (default: 90)', '90')
|
|
31
|
+
.option('--no-cost', 'Skip cost analysis')
|
|
32
|
+
.option('--json', 'Force JSON output')
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
const { runSeatOptimization } = await import('./commands.js');
|
|
35
|
+
await runSeatOptimization(options);
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command('offboard <user>')
|
|
39
|
+
.description('Audit or execute user offboarding')
|
|
40
|
+
.option('--execute', 'Execute the offboarding (default: audit only)')
|
|
41
|
+
.option('--transfer-to <user>', 'Transfer file ownership to this user')
|
|
42
|
+
.option('--json', 'Force JSON output')
|
|
43
|
+
.action(async (user, options) => {
|
|
44
|
+
const { runOffboard } = await import('./commands.js');
|
|
45
|
+
await runOffboard(user, options);
|
|
46
|
+
});
|
|
47
|
+
program
|
|
48
|
+
.command('onboard <email>')
|
|
49
|
+
.description('Invite user to teams and set up access')
|
|
50
|
+
.option('--teams <ids>', 'Comma-separated team IDs', (v) => v.split(','))
|
|
51
|
+
.option('--role <role>', 'Role: editor or viewer (default: editor)', 'editor')
|
|
52
|
+
.option('--share-files <keys>', 'Comma-separated file keys to share (viewer access)', (v) => v.split(','))
|
|
53
|
+
.option('--seat <type>', 'Seat type: full, dev, collab, view')
|
|
54
|
+
.option('--confirm', 'Confirm seat change')
|
|
55
|
+
.option('--json', 'Force JSON output')
|
|
56
|
+
.action(async (email, options) => {
|
|
57
|
+
const { runOnboard } = await import('./commands.js');
|
|
58
|
+
await runOnboard(email, options);
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command('quarterly-report')
|
|
62
|
+
.description('Org-wide design ops snapshot')
|
|
63
|
+
.option('--days <days>', 'Lookback period (default: 90)', '90')
|
|
64
|
+
.option('--json', 'Force JSON output')
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
const { runQuarterlyReport } = await import('./commands.js');
|
|
67
|
+
await runQuarterlyReport(options);
|
|
68
|
+
});
|
|
69
|
+
program
|
|
70
|
+
.command('members')
|
|
71
|
+
.description('List org members')
|
|
72
|
+
.option('--search <query>', 'Filter by name or email')
|
|
73
|
+
.option('--json', 'Force JSON output')
|
|
74
|
+
.action(async (options) => {
|
|
75
|
+
const { runMembers } = await import('./commands.js');
|
|
76
|
+
await runMembers(options);
|
|
77
|
+
});
|
|
78
|
+
program
|
|
79
|
+
.command('teams')
|
|
80
|
+
.description('List org teams')
|
|
81
|
+
.option('--json', 'Force JSON output')
|
|
82
|
+
.action(async (options) => {
|
|
83
|
+
const { runTeams } = await import('./commands.js');
|
|
84
|
+
await runTeams(options);
|
|
85
|
+
});
|
|
86
|
+
program
|
|
87
|
+
.command('permissions <type> <id>')
|
|
88
|
+
.description('Show who has access to a file, project, or team')
|
|
89
|
+
.option('--json', 'Force JSON output')
|
|
90
|
+
.action(async (type, id, options) => {
|
|
91
|
+
const { runPermissions } = await import('./commands.js');
|
|
92
|
+
await runPermissions(type, id, options);
|
|
93
|
+
});
|
|
94
|
+
program
|
|
95
|
+
.command('permission-audit')
|
|
96
|
+
.description('Audit permissions across a team or project')
|
|
97
|
+
.option('--scope <type>', 'Scope: team or project', 'team')
|
|
98
|
+
.option('--id <id>', 'Team or project ID')
|
|
99
|
+
.option('--json', 'Force JSON output')
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
const { runPermissionAudit } = await import('./commands.js');
|
|
102
|
+
await runPermissionAudit(options);
|
|
103
|
+
});
|
|
104
|
+
program
|
|
105
|
+
.command('branch-cleanup <project-id>')
|
|
106
|
+
.description('Find and optionally archive stale branches')
|
|
107
|
+
.option('--days-stale <days>', 'Days threshold (default: 60)', '60')
|
|
108
|
+
.option('--execute', 'Archive stale branches (default: dry run)')
|
|
109
|
+
.option('--json', 'Force JSON output')
|
|
110
|
+
.action(async (projectId, options) => {
|
|
111
|
+
const { runBranchCleanup } = await import('./commands.js');
|
|
112
|
+
await runBranchCleanup(projectId, options);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { platform } from 'node:os';
|
|
3
|
+
import { setActiveWorkspace, deleteConfig, getConfigPath } from '../config.js';
|
|
4
|
+
import { extractCookies, validateSession, validatePat } from '../auth/cookie.js';
|
|
5
|
+
async function prompt(question) {
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
const answer = await new Promise(resolve => rl.question(question, resolve));
|
|
8
|
+
rl.close();
|
|
9
|
+
return answer.trim();
|
|
10
|
+
}
|
|
11
|
+
export async function handleLogin(options = {}) {
|
|
12
|
+
const workspace = {};
|
|
13
|
+
const os = platform();
|
|
14
|
+
// Cookie extraction (unless --pat-only)
|
|
15
|
+
if (!options.patOnly) {
|
|
16
|
+
if (os !== 'darwin' && os !== 'linux' && os !== 'win32') {
|
|
17
|
+
console.log(`Cookie extraction not supported on ${os}. Use --pat-only.`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const promptLabel = os === 'darwin' ? ' (Keychain prompt may appear)' : '';
|
|
21
|
+
console.log(`Reading Chrome cookies${promptLabel}...`);
|
|
22
|
+
try {
|
|
23
|
+
const accounts = extractCookies();
|
|
24
|
+
if (accounts.length === 0) {
|
|
25
|
+
if (os === 'win32') {
|
|
26
|
+
console.log(' No Figma cookies extracted. Windows extraction is best-effort.');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(' No Figma cookies found. Log into figma.com in Chrome.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Pick account if multiple
|
|
34
|
+
let selected = accounts[0];
|
|
35
|
+
if (accounts.length > 1) {
|
|
36
|
+
console.log(`\n Found ${accounts.length} Figma accounts:\n`);
|
|
37
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
38
|
+
console.log(` [${i + 1}] User ${accounts[i].userId} (${accounts[i].profile})`);
|
|
39
|
+
}
|
|
40
|
+
const answer = await prompt(`\n Select account [1-${accounts.length}]: `);
|
|
41
|
+
const idx = parseInt(answer, 10) - 1;
|
|
42
|
+
if (idx < 0 || idx >= accounts.length) {
|
|
43
|
+
console.error(' Invalid selection.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
selected = accounts[idx];
|
|
47
|
+
}
|
|
48
|
+
console.log(` Cookie found for user ${selected.userId}`);
|
|
49
|
+
// Validate session and detect org
|
|
50
|
+
console.log('Validating session...');
|
|
51
|
+
try {
|
|
52
|
+
const session = await validateSession(selected.cookieValue, selected.userId);
|
|
53
|
+
console.log(` Session valid (user ${selected.userId})`);
|
|
54
|
+
if (session.teams.length > 0) {
|
|
55
|
+
console.log(` Teams: ${session.teams.map(t => t.name).join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
workspace.cookie = selected.cookieValue;
|
|
58
|
+
workspace.user_id = selected.userId;
|
|
59
|
+
workspace.cookie_extracted_at = new Date().toISOString();
|
|
60
|
+
// Org selection
|
|
61
|
+
let orgId = session.orgId;
|
|
62
|
+
if (session.orgs.length > 1) {
|
|
63
|
+
console.log(`\n Found ${session.orgs.length} workspaces:\n`);
|
|
64
|
+
for (let i = 0; i < session.orgs.length; i++) {
|
|
65
|
+
const o = session.orgs[i];
|
|
66
|
+
const marker = o.id === orgId ? ' (current)' : '';
|
|
67
|
+
console.log(` [${i + 1}] ${o.name} (${o.id})${marker}`);
|
|
68
|
+
}
|
|
69
|
+
const answer = await prompt(`\n Default workspace [1-${session.orgs.length}] (Enter for 1): `);
|
|
70
|
+
if (answer) {
|
|
71
|
+
const idx = parseInt(answer, 10) - 1;
|
|
72
|
+
if (idx >= 0 && idx < session.orgs.length) {
|
|
73
|
+
orgId = session.orgs[idx].id;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (orgId) {
|
|
78
|
+
workspace.org_id = orgId;
|
|
79
|
+
const orgName = session.orgs.find(o => o.id === orgId)?.name;
|
|
80
|
+
console.log(` Workspace: ${orgName ? `${orgName} (${orgId})` : orgId}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
const status = e.response?.status;
|
|
85
|
+
if (status === 401 || status === 403) {
|
|
86
|
+
console.error(' Cookie expired. Log into figma.com in Chrome and try again.');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.error(` Session validation failed: ${e.message}`);
|
|
90
|
+
}
|
|
91
|
+
// Continue to PAT prompt -- cookie failed but PAT might work
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
if (os === 'win32') {
|
|
97
|
+
console.log(` Cookie extraction failed: ${e.message}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.error(` Cookie extraction failed: ${e.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// PAT prompt
|
|
106
|
+
console.log('\nA Personal Access Token enables comments, export, and version history.');
|
|
107
|
+
console.log('Generate one at: https://www.figma.com/settings (Security > Personal access tokens)');
|
|
108
|
+
const patInput = await prompt('Paste your PAT (or press Enter to skip): ');
|
|
109
|
+
if (patInput) {
|
|
110
|
+
console.log('Validating PAT...');
|
|
111
|
+
try {
|
|
112
|
+
const patUser = await validatePat(patInput);
|
|
113
|
+
console.log(` PAT valid (${patUser})`);
|
|
114
|
+
workspace.pat = patInput;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
console.log(' PAT invalid or expired -- skipping.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Must have at least one credential
|
|
121
|
+
if (!workspace.pat && !workspace.cookie) {
|
|
122
|
+
console.error('\nNo credentials configured. Need at least a PAT or browser cookie.');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
// Derive a workspace name from the org or user
|
|
126
|
+
const workspaceName = workspace.org_id || workspace.user_id || 'default';
|
|
127
|
+
setActiveWorkspace(workspaceName, workspace);
|
|
128
|
+
console.log(`\nCredentials saved to ${getConfigPath()}`);
|
|
129
|
+
console.log('Done. figmanage will use these credentials automatically.');
|
|
130
|
+
}
|
|
131
|
+
export async function handleLogout() {
|
|
132
|
+
deleteConfig();
|
|
133
|
+
console.log('Logged out. Config file removed.');
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { loadAuthConfig, hasPat, hasCookie } from '../auth/client.js';
|
|
3
|
+
export async function handleWhoami() {
|
|
4
|
+
const config = loadAuthConfig();
|
|
5
|
+
if (!hasPat(config) && !hasCookie(config)) {
|
|
6
|
+
console.error('No auth configured. Run `figmanage login` or set environment variables.');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const authMethods = [];
|
|
10
|
+
// Cookie auth: call internal API /api/me
|
|
11
|
+
if (hasCookie(config)) {
|
|
12
|
+
authMethods.push('cookie');
|
|
13
|
+
try {
|
|
14
|
+
const res = await axios.get('https://www.figma.com/api/user/state', {
|
|
15
|
+
headers: {
|
|
16
|
+
'Cookie': `__Host-figma.authn=${config.cookie}`,
|
|
17
|
+
'X-CSRF-Bypass': 'yes',
|
|
18
|
+
'X-Figma-User-Id': config.userId || '',
|
|
19
|
+
},
|
|
20
|
+
timeout: 15000,
|
|
21
|
+
});
|
|
22
|
+
const meta = res.data?.meta || res.data || {};
|
|
23
|
+
const user = meta.user || meta;
|
|
24
|
+
console.log(`User: ${user.handle || user.email || config.userId}`);
|
|
25
|
+
if (user.email)
|
|
26
|
+
console.log(`Email: ${user.email}`);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const status = e.response?.status;
|
|
30
|
+
if (status === 401 || status === 403) {
|
|
31
|
+
console.log('Cookie: expired or invalid');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(`Cookie: request failed (${e.message})`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// PAT auth: call public API /v1/me
|
|
39
|
+
if (hasPat(config)) {
|
|
40
|
+
authMethods.push('PAT');
|
|
41
|
+
try {
|
|
42
|
+
const res = await axios.get('https://api.figma.com/v1/me', {
|
|
43
|
+
headers: { 'X-Figma-Token': config.pat },
|
|
44
|
+
timeout: 15000,
|
|
45
|
+
});
|
|
46
|
+
const user = res.data;
|
|
47
|
+
// Only print user info if we didn't already from cookie
|
|
48
|
+
if (!hasCookie(config)) {
|
|
49
|
+
console.log(`User: ${user.handle || user.email}`);
|
|
50
|
+
if (user.email)
|
|
51
|
+
console.log(`Email: ${user.email}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
const status = e.response?.status;
|
|
56
|
+
if (status === 401 || status === 403) {
|
|
57
|
+
console.log('PAT: expired or invalid');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(`PAT: request failed (${e.message})`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (config.orgId)
|
|
65
|
+
console.log(`Org: ${config.orgId}`);
|
|
66
|
+
console.log(`Auth: ${authMethods.join(' + ')}`);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=whoami.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface WorkspaceConfig {
|
|
2
|
+
cookie?: string;
|
|
3
|
+
user_id?: string;
|
|
4
|
+
org_id?: string;
|
|
5
|
+
pat?: string;
|
|
6
|
+
cookie_extracted_at?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FigmanageConfig {
|
|
9
|
+
workspaces: Record<string, WorkspaceConfig>;
|
|
10
|
+
active_workspace: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function getConfigDir(): string;
|
|
13
|
+
export declare function getConfigPath(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Read and parse the config file. Returns null if not found or malformed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readConfig(): FigmanageConfig | null;
|
|
18
|
+
/**
|
|
19
|
+
* Write config to disk with restricted permissions (0o600).
|
|
20
|
+
* Creates the config directory if it doesn't exist.
|
|
21
|
+
*/
|
|
22
|
+
export declare function writeConfig(config: FigmanageConfig): void;
|
|
23
|
+
/**
|
|
24
|
+
* Return the active workspace entry, or null if no config or workspace found.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getActiveWorkspace(): WorkspaceConfig | null;
|
|
27
|
+
/**
|
|
28
|
+
* Set or update a workspace entry and make it active. Merges with existing config.
|
|
29
|
+
*/
|
|
30
|
+
export declare function setActiveWorkspace(name: string, workspace: WorkspaceConfig): void;
|
|
31
|
+
/**
|
|
32
|
+
* Delete the config file. No-op if it doesn't exist.
|
|
33
|
+
*/
|
|
34
|
+
export declare function deleteConfig(): void;
|
|
35
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function getConfigDir() {
|
|
5
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
6
|
+
return join(process.env.APPDATA, 'figmanage');
|
|
7
|
+
}
|
|
8
|
+
return join(homedir(), '.config', 'figmanage');
|
|
9
|
+
}
|
|
10
|
+
export function getConfigPath() {
|
|
11
|
+
return join(getConfigDir(), 'config.json');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read and parse the config file. Returns null if not found or malformed.
|
|
15
|
+
*/
|
|
16
|
+
export function readConfig() {
|
|
17
|
+
const configPath = getConfigPath();
|
|
18
|
+
if (!existsSync(configPath))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
// Basic shape validation
|
|
24
|
+
if (typeof parsed !== 'object' ||
|
|
25
|
+
parsed === null ||
|
|
26
|
+
typeof parsed.workspaces !== 'object' ||
|
|
27
|
+
parsed.workspaces === null ||
|
|
28
|
+
typeof parsed.active_workspace !== 'string') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Write config to disk with restricted permissions (0o600).
|
|
39
|
+
* Creates the config directory if it doesn't exist.
|
|
40
|
+
*/
|
|
41
|
+
export function writeConfig(config) {
|
|
42
|
+
const configDir = getConfigDir();
|
|
43
|
+
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
44
|
+
const configPath = getConfigPath();
|
|
45
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
46
|
+
// Ensure permissions even if file pre-existed with broader mode
|
|
47
|
+
if (process.platform !== 'win32') {
|
|
48
|
+
chmodSync(configPath, 0o600);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Return the active workspace entry, or null if no config or workspace found.
|
|
53
|
+
*/
|
|
54
|
+
export function getActiveWorkspace() {
|
|
55
|
+
const config = readConfig();
|
|
56
|
+
if (!config)
|
|
57
|
+
return null;
|
|
58
|
+
const workspace = config.workspaces[config.active_workspace];
|
|
59
|
+
return workspace ?? null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Set or update a workspace entry and make it active. Merges with existing config.
|
|
63
|
+
*/
|
|
64
|
+
export function setActiveWorkspace(name, workspace) {
|
|
65
|
+
const existing = readConfig() ?? { workspaces: {}, active_workspace: name };
|
|
66
|
+
existing.workspaces[name] = workspace;
|
|
67
|
+
existing.active_workspace = name;
|
|
68
|
+
writeConfig(existing);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Delete the config file. No-op if it doesn't exist.
|
|
72
|
+
*/
|
|
73
|
+
export function deleteConfig() {
|
|
74
|
+
const configPath = getConfigPath();
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(configPath);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err.code !== 'ENOENT')
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=config.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import './tools/files.js';
|
|
4
|
-
import './tools/projects.js';
|
|
5
|
-
import './tools/permissions.js';
|
|
6
|
-
import './tools/comments.js';
|
|
7
|
-
import './tools/export.js';
|
|
8
|
-
import './tools/versions.js';
|
|
9
|
-
import './tools/branching.js';
|
|
10
|
-
import './tools/components.js';
|
|
11
|
-
import './tools/webhooks.js';
|
|
12
|
-
import './tools/reading.js';
|
|
13
|
-
import './tools/analytics.js';
|
|
14
|
-
import './tools/variables.js';
|
|
15
|
-
import './tools/org.js';
|
|
16
|
-
import './tools/libraries.js';
|
|
17
|
-
import './tools/teams.js';
|
|
18
|
-
import './tools/compound.js';
|
|
19
|
-
import './tools/compound-manager.js';
|
|
2
|
+
export {};
|
|
20
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,106 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// --setup flag: run the interactive setup wizard and exit.
|
|
3
|
+
// Checked before commander parses to preserve existing behavior
|
|
4
|
+
// (setup uses its own interactive prompts).
|
|
3
5
|
if (process.argv.includes('--setup')) {
|
|
4
6
|
await import('./setup.js');
|
|
5
7
|
process.exit(0);
|
|
6
8
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// Import tool modules (side-effect: registers via defineTool)
|
|
14
|
-
import './tools/navigate.js';
|
|
15
|
-
import './tools/files.js';
|
|
16
|
-
import './tools/projects.js';
|
|
17
|
-
import './tools/permissions.js';
|
|
18
|
-
import './tools/comments.js';
|
|
19
|
-
import './tools/export.js';
|
|
20
|
-
import './tools/versions.js';
|
|
21
|
-
import './tools/branching.js';
|
|
22
|
-
import './tools/components.js';
|
|
23
|
-
import './tools/webhooks.js';
|
|
24
|
-
import './tools/reading.js';
|
|
25
|
-
import './tools/analytics.js';
|
|
26
|
-
import './tools/variables.js';
|
|
27
|
-
import './tools/org.js';
|
|
28
|
-
import './tools/libraries.js';
|
|
29
|
-
import './tools/teams.js';
|
|
30
|
-
import './tools/compound.js';
|
|
31
|
-
import './tools/compound-manager.js';
|
|
32
|
-
const ALL_TOOLSETS = [
|
|
33
|
-
'navigate', 'files', 'projects', 'permissions', 'org',
|
|
34
|
-
'versions', 'branching', 'comments', 'export',
|
|
35
|
-
'analytics', 'reading', 'components', 'webhooks', 'variables',
|
|
36
|
-
'compound', 'teams', 'libraries',
|
|
37
|
-
];
|
|
38
|
-
const TOOLSET_PRESETS = {
|
|
39
|
-
starter: ['navigate', 'reading', 'comments', 'export'],
|
|
40
|
-
admin: ['navigate', 'org', 'permissions', 'analytics', 'teams', 'libraries'],
|
|
41
|
-
readonly: ['navigate', 'reading', 'comments', 'export', 'components', 'versions'],
|
|
42
|
-
full: ALL_TOOLSETS,
|
|
43
|
-
};
|
|
44
|
-
function parseToolsets(env) {
|
|
45
|
-
if (!env)
|
|
46
|
-
return new Set(ALL_TOOLSETS);
|
|
47
|
-
if (env in TOOLSET_PRESETS)
|
|
48
|
-
return new Set(TOOLSET_PRESETS[env]);
|
|
49
|
-
const requested = env.split(',').map(s => s.trim());
|
|
50
|
-
const valid = requested.filter(t => ALL_TOOLSETS.includes(t));
|
|
51
|
-
return new Set(valid.length > 0 ? valid : ALL_TOOLSETS);
|
|
9
|
+
// --mcp flag: start the MCP server (stdio or HTTP).
|
|
10
|
+
// This is the hot path for MCP clients -- tool modules are only
|
|
11
|
+
// loaded inside startMcpServer(), keeping CLI startup fast.
|
|
12
|
+
if (process.argv.includes('--mcp')) {
|
|
13
|
+
const { startMcpServer } = await import('./mcp.js');
|
|
14
|
+
await startMcpServer();
|
|
52
15
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
16
|
+
else {
|
|
17
|
+
// CLI mode: parse commands with commander
|
|
18
|
+
const { Command } = await import('commander');
|
|
19
|
+
const { registerCliCommands } = await import('./cli/index.js');
|
|
20
|
+
const program = new Command();
|
|
21
|
+
program
|
|
22
|
+
.name('figmanage')
|
|
23
|
+
.description('Figma workspace management CLI')
|
|
24
|
+
.version(JSON.parse((await import('node:fs')).readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version);
|
|
25
|
+
registerCliCommands(program);
|
|
26
|
+
// No subcommand given -- show help
|
|
27
|
+
if (process.argv.length <= 2) {
|
|
28
|
+
program.outputHelp();
|
|
29
|
+
process.exit(0);
|
|
61
30
|
}
|
|
62
|
-
|
|
31
|
+
await program.parseAsync(process.argv);
|
|
63
32
|
}
|
|
64
|
-
|
|
65
|
-
const config = loadAuthConfig();
|
|
66
|
-
const readOnly = process.env.FIGMA_READ_ONLY === '1' || process.env.FIGMA_READ_ONLY === 'true';
|
|
67
|
-
const enabledToolsets = parseToolsets(process.env.FIGMA_TOOLSETS);
|
|
68
|
-
if (!hasPat(config) && !hasCookie(config)) {
|
|
69
|
-
console.error('No auth configured. Set FIGMA_PAT for public API access, or ' +
|
|
70
|
-
'FIGMA_AUTH_COOKIE + FIGMA_USER_ID for internal API access.');
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
const server = new McpServer({
|
|
74
|
-
name: 'figmanage',
|
|
75
|
-
version: '0.1.0',
|
|
76
|
-
});
|
|
77
|
-
registerTools(server, config, enabledToolsets, readOnly);
|
|
78
|
-
const httpPort = parseHttpPort(process.argv);
|
|
79
|
-
if (httpPort) {
|
|
80
|
-
const transport = new StreamableHTTPServerTransport({
|
|
81
|
-
sessionIdGenerator: undefined,
|
|
82
|
-
});
|
|
83
|
-
const httpServer = createServer(async (req, res) => {
|
|
84
|
-
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
85
|
-
if (url.pathname === '/mcp') {
|
|
86
|
-
await transport.handleRequest(req, res);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
res.writeHead(404).end('Not found');
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
await server.connect(transport);
|
|
93
|
-
httpServer.listen(httpPort, () => {
|
|
94
|
-
console.error(`figmanage HTTP server listening on http://localhost:${httpPort}/mcp`);
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
const transport = new StdioServerTransport();
|
|
99
|
-
await server.connect(transport);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
main().catch((err) => {
|
|
103
|
-
console.error('Fatal:', err);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
});
|
|
33
|
+
export {};
|
|
106
34
|
//# sourceMappingURL=index.js.map
|