figmanage 1.1.0 → 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.
@@ -0,0 +1,7 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Create the `completion` command. Needs the parent program reference
4
+ * so it can introspect the full command tree at runtime.
5
+ */
6
+ export declare function completionCommand(program: Command): Command;
7
+ //# sourceMappingURL=completion.d.ts.map
@@ -0,0 +1,160 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Introspect a Commander program and extract its command tree.
4
+ * Returns a map of command names to their subcommand names.
5
+ * Top-level commands without subcommands (like login, whoami) get an empty array.
6
+ */
7
+ function extractCommandTree(program) {
8
+ const tree = new Map();
9
+ for (const cmd of program.commands) {
10
+ const subs = cmd.commands.map((sub) => sub.name());
11
+ tree.set(cmd.name(), subs);
12
+ }
13
+ return tree;
14
+ }
15
+ /**
16
+ * Extract all option flags from a command (including inherited ones).
17
+ * Returns long-form flags like --json, --help.
18
+ */
19
+ function extractOptions(cmd) {
20
+ const flags = [];
21
+ for (const opt of cmd.options) {
22
+ if (opt.long)
23
+ flags.push(opt.long);
24
+ }
25
+ return flags;
26
+ }
27
+ /**
28
+ * Collect global options that appear across most commands.
29
+ * These get offered at every completion point.
30
+ */
31
+ function extractGlobalOptions(program) {
32
+ const flags = new Set();
33
+ // Program-level options (--version, etc.)
34
+ for (const opt of program.options) {
35
+ if (opt.long)
36
+ flags.add(opt.long);
37
+ }
38
+ // Common flags present on subcommands
39
+ flags.add('--help');
40
+ flags.add('--json');
41
+ return [...flags];
42
+ }
43
+ function generateZshScript(program) {
44
+ const tree = extractCommandTree(program);
45
+ const globalOpts = extractGlobalOptions(program);
46
+ const topLevelCmds = [...tree.keys()];
47
+ // Build case arms for subcommand completion
48
+ const caseArms = [];
49
+ for (const [group, subs] of tree) {
50
+ if (subs.length > 0) {
51
+ caseArms.push(` ${group})\n local subcmds=(${subs.join(' ')})\n _describe 'subcommand' subcmds\n ;;`);
52
+ }
53
+ }
54
+ return `#compdef figmanage
55
+
56
+ # Shell completion for figmanage
57
+ # Add to ~/.zshrc: eval "$(figmanage completion)"
58
+
59
+ _figmanage() {
60
+ local -a commands
61
+ commands=(${topLevelCmds.join(' ')})
62
+
63
+ local global_opts=(${globalOpts.join(' ')})
64
+
65
+ _arguments -C \\
66
+ '1:command:->cmd' \\
67
+ '2:subcommand:->sub' \\
68
+ '*::options:->opts'
69
+
70
+ case $state in
71
+ cmd)
72
+ _describe 'command' commands
73
+ ;;
74
+ sub)
75
+ case $words[1] in
76
+ ${caseArms.join('\n')}
77
+ *)
78
+ _describe 'option' global_opts
79
+ ;;
80
+ esac
81
+ ;;
82
+ opts)
83
+ _values 'options' $global_opts
84
+ ;;
85
+ esac
86
+ }
87
+
88
+ compdef _figmanage figmanage
89
+ `;
90
+ }
91
+ function generateBashScript(program) {
92
+ const tree = extractCommandTree(program);
93
+ const globalOpts = extractGlobalOptions(program);
94
+ const topLevelCmds = [...tree.keys()];
95
+ // Build case arms for subcommand completion
96
+ const caseArms = [];
97
+ for (const [group, subs] of tree) {
98
+ if (subs.length > 0) {
99
+ caseArms.push(` ${group})\n COMPREPLY=($(compgen -W "${subs.join(' ')}" -- "$cur"))\n ;;`);
100
+ }
101
+ }
102
+ return `# Shell completion for figmanage
103
+ # Add to ~/.bashrc: eval "$(figmanage completion)"
104
+
105
+ _figmanage() {
106
+ local cur prev words cword
107
+ _init_completion || return
108
+
109
+ local commands="${topLevelCmds.join(' ')}"
110
+ local global_opts="${globalOpts.join(' ')}"
111
+
112
+ case $cword in
113
+ 1)
114
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
115
+ ;;
116
+ 2)
117
+ case "\${words[1]}" in
118
+ ${caseArms.join('\n')}
119
+ *)
120
+ COMPREPLY=($(compgen -W "$global_opts" -- "$cur"))
121
+ ;;
122
+ esac
123
+ ;;
124
+ *)
125
+ COMPREPLY=($(compgen -W "$global_opts" -- "$cur"))
126
+ ;;
127
+ esac
128
+ }
129
+
130
+ complete -F _figmanage figmanage
131
+ `;
132
+ }
133
+ function detectShell() {
134
+ const shell = process.env.SHELL ?? '';
135
+ if (shell.endsWith('/zsh'))
136
+ return 'zsh';
137
+ return 'bash';
138
+ }
139
+ /**
140
+ * Create the `completion` command. Needs the parent program reference
141
+ * so it can introspect the full command tree at runtime.
142
+ */
143
+ export function completionCommand(program) {
144
+ const cmd = new Command('completion')
145
+ .description('Output shell completion script')
146
+ .option('--shell <shell>', 'Shell type (bash or zsh)')
147
+ .action((options) => {
148
+ const shell = options.shell ?? detectShell();
149
+ if (shell !== 'bash' && shell !== 'zsh') {
150
+ console.error(`Unsupported shell: ${shell}. Use --shell bash or --shell zsh.`);
151
+ process.exit(1);
152
+ }
153
+ const script = shell === 'zsh'
154
+ ? generateZshScript(program)
155
+ : generateBashScript(program);
156
+ process.stdout.write(script);
157
+ });
158
+ return cmd;
159
+ }
160
+ //# sourceMappingURL=completion.js.map
@@ -2,13 +2,158 @@
2
2
  export function isTTY() {
3
3
  return process.stdout.isTTY === true;
4
4
  }
5
+ /** Get terminal width, defaulting to 80 if unavailable */
6
+ function termWidth() {
7
+ return process.stdout.columns || 80;
8
+ }
9
+ /** Format a value for display in a table cell */
10
+ function formatCell(value) {
11
+ if (value === null || value === undefined)
12
+ return '';
13
+ if (typeof value === 'string')
14
+ return value;
15
+ if (typeof value === 'number' || typeof value === 'boolean')
16
+ return String(value);
17
+ if (Array.isArray(value))
18
+ return `[${value.length} items]`;
19
+ if (typeof value === 'object')
20
+ return '{...}';
21
+ return String(value);
22
+ }
23
+ /** Truncate a string to maxLen, appending ... if truncated */
24
+ function truncate(str, maxLen) {
25
+ if (maxLen < 4)
26
+ return str.slice(0, maxLen);
27
+ if (str.length <= maxLen)
28
+ return str;
29
+ return str.slice(0, maxLen - 3) + '...';
30
+ }
31
+ /** Pad a string to the right with spaces */
32
+ function padRight(str, width) {
33
+ if (str.length >= width)
34
+ return str;
35
+ return str + ' '.repeat(width - str.length);
36
+ }
37
+ /**
38
+ * Render an array of objects as an aligned table.
39
+ * Columns are auto-sized to content, then shrunk proportionally if they
40
+ * exceed terminal width. A minimum gap of 2 spaces separates columns.
41
+ */
42
+ function formatTable(rows) {
43
+ if (rows.length === 0)
44
+ return '';
45
+ // Collect all keys across all rows (preserving insertion order)
46
+ const keySet = new Set();
47
+ for (const row of rows) {
48
+ for (const key of Object.keys(row)) {
49
+ keySet.add(key);
50
+ }
51
+ }
52
+ const keys = Array.from(keySet);
53
+ // Build string grid: headers + data
54
+ const headers = keys.map((k) => k.toUpperCase());
55
+ const grid = rows.map((row) => keys.map((k) => formatCell(row[k])));
56
+ // Compute natural column widths (max of header and all data cells)
57
+ const colWidths = keys.map((_, i) => {
58
+ let max = headers[i].length;
59
+ for (const row of grid) {
60
+ if (row[i].length > max)
61
+ max = row[i].length;
62
+ }
63
+ return max;
64
+ });
65
+ const gap = 2;
66
+ const maxWidth = termWidth();
67
+ const totalGap = gap * (keys.length - 1);
68
+ const totalNatural = colWidths.reduce((a, b) => a + b, 0) + totalGap;
69
+ // If columns exceed terminal width, shrink the widest columns first
70
+ if (totalNatural > maxWidth && keys.length > 0) {
71
+ const budget = maxWidth - totalGap;
72
+ if (budget > 0) {
73
+ // Give each column at least 4 chars, distribute remaining proportionally
74
+ const minCol = 4;
75
+ const guaranteed = Math.min(minCol, Math.floor(budget / keys.length));
76
+ let remaining = budget;
77
+ // First pass: cap each column to its natural width or proportional share
78
+ const totalContent = colWidths.reduce((a, b) => a + b, 0);
79
+ for (let i = 0; i < colWidths.length; i++) {
80
+ const share = Math.max(guaranteed, Math.floor((colWidths[i] / totalContent) * budget));
81
+ colWidths[i] = Math.min(colWidths[i], share);
82
+ remaining -= colWidths[i];
83
+ }
84
+ // Distribute leftover to columns that were truncated
85
+ if (remaining > 0) {
86
+ for (let i = 0; i < colWidths.length && remaining > 0; i++) {
87
+ const natural = headers[i].length;
88
+ const canGrow = Math.max(0, natural - colWidths[i]);
89
+ const give = Math.min(canGrow, remaining);
90
+ colWidths[i] += give;
91
+ remaining -= give;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ // Render rows
97
+ const lines = [];
98
+ // Header row
99
+ const headerLine = keys
100
+ .map((_, i) => padRight(truncate(headers[i], colWidths[i]), colWidths[i]))
101
+ .join(' '.repeat(gap));
102
+ lines.push(headerLine.trimEnd());
103
+ // Data rows
104
+ for (const row of grid) {
105
+ const line = keys
106
+ .map((_, i) => padRight(truncate(row[i], colWidths[i]), colWidths[i]))
107
+ .join(' '.repeat(gap));
108
+ lines.push(line.trimEnd());
109
+ }
110
+ return lines.join('\n');
111
+ }
112
+ /**
113
+ * Render a single object as key-value pairs.
114
+ * Falls back to JSON for deeply nested objects.
115
+ */
116
+ function formatKeyValue(obj) {
117
+ // If every value is a nested object/array, this won't be readable as k/v.
118
+ // Check if the majority of values are complex -- if so, fall back to JSON.
119
+ const values = Object.values(obj);
120
+ const complexCount = values.filter((v) => v !== null && typeof v === 'object').length;
121
+ if (complexCount > values.length / 2) {
122
+ return JSON.stringify(obj, null, 2);
123
+ }
124
+ const entries = Object.entries(obj);
125
+ if (entries.length === 0)
126
+ return '{}';
127
+ return entries.map(([key, val]) => `${key}: ${formatCell(val)}`).join('\n');
128
+ }
5
129
  /** Format output: JSON if piped or --json flag, human-readable if TTY */
6
130
  export function formatOutput(data, options) {
7
131
  if (options.json || !isTTY()) {
8
132
  return JSON.stringify(data, null, 2);
9
133
  }
10
- // Table formatting will be added later; fall back to JSON for now
11
- return JSON.stringify(data, null, 2);
134
+ // Primitives: render as-is
135
+ if (data === null || data === undefined)
136
+ return '';
137
+ if (typeof data === 'string')
138
+ return data;
139
+ if (typeof data === 'number' || typeof data === 'boolean')
140
+ return String(data);
141
+ // Array of objects: table
142
+ if (Array.isArray(data)) {
143
+ if (data.length === 0)
144
+ return '';
145
+ // If every element is a plain object, render as table
146
+ if (data.every((item) => item !== null && typeof item === 'object' && !Array.isArray(item))) {
147
+ return formatTable(data);
148
+ }
149
+ // Array of primitives or mixed: one per line
150
+ return data.map((item) => formatCell(item)).join('\n');
151
+ }
152
+ // Single object
153
+ if (typeof data === 'object') {
154
+ return formatKeyValue(data);
155
+ }
156
+ return String(data);
12
157
  }
13
158
  /** Print formatted output to stdout */
14
159
  export function output(data, options = {}) {
package/dist/cli/index.js CHANGED
@@ -14,6 +14,7 @@ import { analyticsCommand } from './analytics.js';
14
14
  import { orgCommand } from './org.js';
15
15
  import { librariesCommand } from './libraries.js';
16
16
  import { teamsCommand } from './teams.js';
17
+ import { completionCommand } from './completion.js';
17
18
  import { fileSummaryCommand, workspaceOverviewCommand, openCommentsCommand, cleanupStaleFilesCommand, organizeProjectCommand, setupProjectStructureCommand, seatOptimizationCommand, permissionAuditCommand, branchCleanupCommand, offboardUserCommand, onboardUserCommand, quarterlyReportCommand, } from './compound-commands.js';
18
19
  export function registerCliCommands(program) {
19
20
  // Auth commands (flat -- not resource-scoped)
@@ -87,5 +88,7 @@ export function registerCliCommands(program) {
87
88
  program.addCommand(org);
88
89
  program.addCommand(libraries);
89
90
  program.addCommand(teams);
91
+ // Completion must be registered last so it can introspect all commands above
92
+ program.addCommand(completionCommand(program));
90
93
  }
91
94
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "figmanage",
3
3
  "mcpName": "io.github.dannykeane/figmanage",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "description": "MCP server for managing your Figma workspace from the terminal.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",