@akta/dao-cli 0.1.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/src/index.ts ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ import type { AkitaNetwork } from "@akta/sdk";
4
+ import { createDAO } from "./sdk";
5
+ import { infoCommand } from "./commands/info";
6
+ import { stateCommand } from "./commands/state";
7
+ import { listProposals, getProposal } from "./commands/proposals";
8
+ import {
9
+ walletInfo,
10
+ walletPlugins,
11
+ walletNamedPlugins,
12
+ walletEscrows,
13
+ walletAllowances,
14
+ walletExecutions,
15
+ walletBalance,
16
+ } from "./commands/wallet";
17
+ import { startTUI } from "./tui/app";
18
+
19
+ const program = new Command();
20
+
21
+ program
22
+ .name("akita-dao")
23
+ .description("Read-only CLI for querying Akita DAO state on Algorand")
24
+ .version("0.1.0")
25
+ .option("-n, --network <network>", "Network to connect to", "mainnet")
26
+ .option("-j, --json", "Output as JSON", false);
27
+
28
+ function getOpts(): { network: AkitaNetwork; json: boolean } {
29
+ const opts = program.opts();
30
+ const network = opts.network as AkitaNetwork;
31
+ if (!["mainnet", "testnet", "localnet"].includes(network)) {
32
+ console.error(`Invalid network: ${network}. Use mainnet, testnet, or localnet.`);
33
+ process.exit(1);
34
+ }
35
+ return { network, json: opts.json };
36
+ }
37
+
38
+ // tui
39
+ program
40
+ .command("tui")
41
+ .description("Launch interactive full-screen TUI")
42
+ .action(async () => {
43
+ const { network } = getOpts();
44
+ await startTUI(network);
45
+ });
46
+
47
+ // info
48
+ program
49
+ .command("info")
50
+ .description("Quick DAO dashboard (app IDs, assets, version)")
51
+ .action(async () => {
52
+ const { network, json } = getOpts();
53
+ const dao = createDAO(network);
54
+ await infoCommand(dao, network, json);
55
+ });
56
+
57
+ // state
58
+ program
59
+ .command("state")
60
+ .description("Full decoded global state")
61
+ .action(async () => {
62
+ const { network, json } = getOpts();
63
+ const dao = createDAO(network);
64
+ await stateCommand(dao, network, json);
65
+ });
66
+
67
+ // proposals
68
+ const proposals = program
69
+ .command("proposals")
70
+ .description("Proposal commands");
71
+
72
+ proposals
73
+ .command("list")
74
+ .description("List proposals")
75
+ .option("-s, --status <status>", "Filter by status: all, active, past", "all")
76
+ .option("-l, --limit <limit>", "Max proposals to show", "20")
77
+ .action(async (opts) => {
78
+ const { network, json } = getOpts();
79
+ const dao = createDAO(network);
80
+ await listProposals(dao, json, opts.status, parseInt(opts.limit, 10));
81
+ });
82
+
83
+ proposals
84
+ .command("get <id>")
85
+ .description("Get detailed proposal view")
86
+ .action(async (id: string) => {
87
+ const { network, json } = getOpts();
88
+ const dao = createDAO(network);
89
+ await getProposal(dao, BigInt(id), json);
90
+ });
91
+
92
+ // wallet
93
+ const walletCmd = program
94
+ .command("wallet")
95
+ .description("Wallet commands");
96
+
97
+ walletCmd
98
+ .command("info")
99
+ .description("Wallet global state")
100
+ .action(async () => {
101
+ const { network, json } = getOpts();
102
+ const dao = createDAO(network);
103
+ await walletInfo(dao, network, json);
104
+ });
105
+
106
+ walletCmd
107
+ .command("plugins")
108
+ .description("All installed plugins")
109
+ .action(async () => {
110
+ const { network, json } = getOpts();
111
+ const dao = createDAO(network);
112
+ await walletPlugins(dao, network, json);
113
+ });
114
+
115
+ walletCmd
116
+ .command("named-plugins")
117
+ .description("Named plugin aliases")
118
+ .action(async () => {
119
+ const { network, json } = getOpts();
120
+ const dao = createDAO(network);
121
+ await walletNamedPlugins(dao, json);
122
+ });
123
+
124
+ walletCmd
125
+ .command("escrows")
126
+ .description("All escrows")
127
+ .action(async () => {
128
+ const { network, json } = getOpts();
129
+ const dao = createDAO(network);
130
+ await walletEscrows(dao, json);
131
+ });
132
+
133
+ walletCmd
134
+ .command("allowances")
135
+ .description("All spending allowances")
136
+ .action(async () => {
137
+ const { network, json } = getOpts();
138
+ const dao = createDAO(network);
139
+ await walletAllowances(dao, json);
140
+ });
141
+
142
+ walletCmd
143
+ .command("executions")
144
+ .description("All execution keys")
145
+ .action(async () => {
146
+ const { network, json } = getOpts();
147
+ const dao = createDAO(network);
148
+ await walletExecutions(dao, json);
149
+ });
150
+
151
+ walletCmd
152
+ .command("balance [assets...]")
153
+ .description("Check balances (default: ALGO, AKTA, BONES)")
154
+ .option("-e, --escrow <name>", "Check escrow balance instead of main wallet")
155
+ .action(async (assets: string[], opts: { escrow?: string }) => {
156
+ const { network, json } = getOpts();
157
+ const dao = createDAO(network);
158
+ await walletBalance(dao, network, json, assets, opts.escrow);
159
+ });
160
+
161
+ // Hoist global flags (e.g. -n testnet) before the subcommand so Commander parses them
162
+ const GLOBAL_VALUE_FLAGS = new Set(["-n", "--network"]);
163
+ const GLOBAL_BOOL_FLAGS = new Set(["-j", "--json"]);
164
+
165
+ function hoistGlobalFlags(args: string[]): string[] {
166
+ const flags: string[] = [];
167
+ const rest: string[] = [];
168
+ for (let i = 0; i < args.length; i++) {
169
+ if (GLOBAL_VALUE_FLAGS.has(args[i]) && i + 1 < args.length) {
170
+ flags.push(args[i], args[i + 1]);
171
+ i++;
172
+ } else if (GLOBAL_BOOL_FLAGS.has(args[i])) {
173
+ flags.push(args[i]);
174
+ } else {
175
+ rest.push(args[i]);
176
+ }
177
+ }
178
+ return [...flags, ...rest];
179
+ }
180
+
181
+ const userArgs = hoistGlobalFlags(process.argv.slice(2));
182
+ const hasCommand = userArgs.some((arg) => !arg.startsWith("-"));
183
+
184
+ if (!hasCommand && process.stdout.isTTY) {
185
+ program.parse([...process.argv.slice(0, 2), ...userArgs, "tui"]);
186
+ } else {
187
+ program.parse([...process.argv.slice(0, 2), ...userArgs]);
188
+ }
package/src/output.ts ADDED
@@ -0,0 +1,232 @@
1
+ import theme from "./theme";
2
+
3
+ // ── Internal helpers ─────────────────────────────────────────────
4
+
5
+ /** Strip ANSI escape codes for accurate visible-length calculation */
6
+ export function visibleLength(str: string): number {
7
+ // eslint-disable-next-line no-control-regex
8
+ return str.replace(/\x1b\[[0-9;]*m/g, "").length;
9
+ }
10
+
11
+ /** padEnd that accounts for invisible ANSI escape codes */
12
+ export function padEndVisible(str: string, len: number): string {
13
+ const diff = len - visibleLength(str);
14
+ return diff > 0 ? str + " ".repeat(diff) : str;
15
+ }
16
+
17
+ /** Truncate a string that may contain ANSI codes to a visible width */
18
+ export function truncateAnsi(str: string, maxWidth: number): string {
19
+ let visible = 0;
20
+ let i = 0;
21
+ let result = "";
22
+
23
+ while (i < str.length && visible < maxWidth) {
24
+ // Check for ANSI escape sequence
25
+ if (str[i] === "\x1b" && str[i + 1] === "[") {
26
+ const end = str.indexOf("m", i);
27
+ if (end !== -1) {
28
+ result += str.slice(i, end + 1);
29
+ i = end + 1;
30
+ continue;
31
+ }
32
+ }
33
+ result += str[i];
34
+ visible++;
35
+ i++;
36
+ }
37
+
38
+ // Reset ANSI at end
39
+ return result + "\x1b[0m";
40
+ }
41
+
42
+ /** Terminal width with fallback for piped/non-TTY output */
43
+ function termWidth(): number {
44
+ return process.stdout.columns || 120;
45
+ }
46
+
47
+ // ── JSON output (unchanged) ──────────────────────────────────────
48
+
49
+ export function jsonReplacer(_key: string, value: unknown): unknown {
50
+ if (typeof value === "bigint") return value.toString();
51
+ if (value instanceof Uint8Array) return Buffer.from(value).toString("hex");
52
+ return value;
53
+ }
54
+
55
+ export function printJson(data: unknown): void {
56
+ console.log(JSON.stringify(data, jsonReplacer, 2));
57
+ }
58
+
59
+ // ── Section header ───────────────────────────────────────────────
60
+
61
+ export function renderHeader(text: string, width?: number): string[] {
62
+ const w = width ?? termWidth();
63
+ const prefix = "── ";
64
+ const suffix = " ";
65
+ const remaining = w - prefix.length - text.length - suffix.length;
66
+ const rule = remaining > 0 ? "─".repeat(remaining) : "";
67
+ return [
68
+ "",
69
+ `${theme.label(prefix)}${theme.sectionHeader(text)}${theme.label(suffix + rule)}`,
70
+ ];
71
+ }
72
+
73
+ export function header(text: string): void {
74
+ for (const line of renderHeader(text)) console.log(line);
75
+ }
76
+
77
+ // ── Key-Value pairs ──────────────────────────────────────────────
78
+
79
+ export function renderKV(pairs: [string, unknown][]): string[] {
80
+ if (pairs.length === 0) return [];
81
+ const maxKeyLen = Math.max(...pairs.map(([k]) => k.length));
82
+ return pairs.map(([key, value]) => {
83
+ const paddedKey = key.padEnd(maxKeyLen);
84
+ return ` ${theme.label(paddedKey)} ${String(value)}`;
85
+ });
86
+ }
87
+
88
+ export function printKV(pairs: [string, unknown][]): void {
89
+ for (const line of renderKV(pairs)) console.log(line);
90
+ }
91
+
92
+ // ── Data table (borderless aligned columns) ──────────────────────
93
+
94
+ export function renderColumns(headers: string[], rows: string[][]): string[] {
95
+ if (rows.length === 0) return [];
96
+
97
+ const colWidths = headers.map((h, i) => {
98
+ const dataMax = rows.reduce((max, row) => Math.max(max, visibleLength(row[i] ?? "")), 0);
99
+ return Math.max(h.length, dataMax);
100
+ });
101
+
102
+ const gap = 3;
103
+ const lines: string[] = [];
104
+
105
+ const headerLine = headers
106
+ .map((h, i) => padEndVisible(theme.label(h.toUpperCase()), colWidths[i]))
107
+ .join(" ".repeat(gap));
108
+ lines.push(` ${headerLine}`);
109
+
110
+ for (const row of rows) {
111
+ const line = row
112
+ .map((cell, i) => {
113
+ if (i === row.length - 1) return cell;
114
+ return padEndVisible(cell, colWidths[i]);
115
+ })
116
+ .join(" ".repeat(gap));
117
+ lines.push(` ${line}`);
118
+ }
119
+
120
+ return lines;
121
+ }
122
+
123
+ export function printColumns(headers: string[], rows: string[][]): void {
124
+ for (const line of renderColumns(headers, rows)) console.log(line);
125
+ }
126
+
127
+ // ── Multi-column KV (side-by-side fee groups) ────────────────────
128
+
129
+ export interface KVGroup {
130
+ title: string;
131
+ pairs: [string, string][];
132
+ }
133
+
134
+ export function renderMultiColumnKV(groups: KVGroup[], width?: number): string[] {
135
+ if (groups.length === 0) return [];
136
+
137
+ const w = width ?? termWidth();
138
+ const groupGap = 4;
139
+ const indent = 2;
140
+ const lines: string[] = [];
141
+
142
+ let cols: number;
143
+ if (w >= 120) cols = Math.min(3, groups.length);
144
+ else if (w >= 80) cols = Math.min(2, groups.length);
145
+ else cols = 1;
146
+
147
+ for (let i = 0; i < groups.length; i += cols) {
148
+ const chunk = groups.slice(i, i + cols);
149
+
150
+ const groupMetas = chunk.map((g) => {
151
+ const maxKey = Math.max(g.title.length, ...g.pairs.map(([k]) => k.length));
152
+ const maxVal = Math.max(...g.pairs.map(([, v]) => visibleLength(v)), 0);
153
+ return { group: g, keyWidth: maxKey, valWidth: maxVal, totalWidth: maxKey + 2 + maxVal };
154
+ });
155
+
156
+ const titleLine = groupMetas
157
+ .map((m, idx) => {
158
+ const title = theme.label(m.group.title.toUpperCase());
159
+ if (idx === groupMetas.length - 1) return title;
160
+ return padEndVisible(title, m.totalWidth);
161
+ })
162
+ .join(" ".repeat(groupGap));
163
+ lines.push(" ".repeat(indent) + titleLine);
164
+
165
+ const maxRows = Math.max(...groupMetas.map((m) => m.group.pairs.length));
166
+ for (let r = 0; r < maxRows; r++) {
167
+ const parts = groupMetas.map((m, idx) => {
168
+ const pair = m.group.pairs[r];
169
+ if (!pair) {
170
+ if (idx === groupMetas.length - 1) return "";
171
+ return " ".repeat(m.totalWidth);
172
+ }
173
+ const [key, val] = pair;
174
+ const paddedKey = theme.label(key.padEnd(m.keyWidth));
175
+ const paddedVal = padEndVisible(val, m.valWidth);
176
+ if (idx === groupMetas.length - 1) return `${paddedKey} ${val}`;
177
+ return `${paddedKey} ${paddedVal}`;
178
+ });
179
+ const line = parts.join(" ".repeat(groupGap));
180
+ lines.push(" ".repeat(indent) + line);
181
+ }
182
+ }
183
+
184
+ return lines;
185
+ }
186
+
187
+ export function printMultiColumnKV(groups: KVGroup[]): void {
188
+ for (const line of renderMultiColumnKV(groups)) console.log(line);
189
+ }
190
+
191
+ // ── Plugin card (compact inline) ─────────────────────────────────
192
+
193
+ export interface PluginCardOpts {
194
+ name: string;
195
+ pairs: [string, string][];
196
+ methods?: string[];
197
+ }
198
+
199
+ export function renderPluginCard(opts: PluginCardOpts, width?: number): string[] {
200
+ const w = width ?? termWidth();
201
+ const indentStr = " ";
202
+ const maxLineWidth = w - indentStr.length;
203
+ const sep = " ";
204
+ const lines: string[] = [];
205
+
206
+ lines.push("");
207
+ lines.push(` ${theme.sectionHeader(opts.name)}`);
208
+
209
+ const items = opts.pairs.map(([k, v]) => `${theme.label(k + ":")} ${v}`);
210
+ let current = "";
211
+
212
+ for (const item of items) {
213
+ const candidateLen = current ? visibleLength(current) + sep.length + visibleLength(item) : visibleLength(item);
214
+ if (current && candidateLen > maxLineWidth) {
215
+ lines.push(indentStr + current);
216
+ current = item;
217
+ } else {
218
+ current = current ? current + sep + item : item;
219
+ }
220
+ }
221
+ if (current) lines.push(indentStr + current);
222
+
223
+ if (opts.methods && opts.methods.length > 0) {
224
+ lines.push(indentStr + theme.label("Methods:") + " " + opts.methods.join(", "));
225
+ }
226
+
227
+ return lines;
228
+ }
229
+
230
+ export function printPluginCard(opts: PluginCardOpts): void {
231
+ for (const line of renderPluginCard(opts)) console.log(line);
232
+ }
package/src/sdk.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { AlgorandClient } from "@algorandfoundation/algokit-utils/types/algorand-client";
2
+ import { Address, makeEmptyTransactionSigner } from "algosdk";
3
+ import { setCurrentNetwork, type AkitaNetwork, AkitaDaoSDK } from "@akta/sdk";
4
+
5
+ const ALGOD_URLS: Record<AkitaNetwork, string> = {
6
+ mainnet: "https://mainnet-api.algonode.cloud",
7
+ testnet: "https://testnet-api.algonode.cloud",
8
+ localnet: "http://localhost",
9
+ };
10
+
11
+ const ALGOD_PORTS: Record<AkitaNetwork, number> = {
12
+ mainnet: 443,
13
+ testnet: 443,
14
+ localnet: 4001,
15
+ };
16
+
17
+ export function createDAO(network: AkitaNetwork): AkitaDaoSDK {
18
+ setCurrentNetwork(network);
19
+
20
+ const algorand = AlgorandClient.fromConfig({
21
+ algodConfig: {
22
+ server: ALGOD_URLS[network],
23
+ port: ALGOD_PORTS[network],
24
+ token: "",
25
+ },
26
+ });
27
+
28
+ const READER = Address.fromString("Y76M3MSY6DKBRHBL7C3NNDXGS5IIMQVQVUAB6MP4XEMMGVF2QWNPL226CA");
29
+
30
+ return new AkitaDaoSDK({
31
+ algorand,
32
+ factoryParams: {
33
+ defaultSender: READER,
34
+ defaultSigner: makeEmptyTransactionSigner(),
35
+ },
36
+ });
37
+ }
package/src/theme.ts ADDED
@@ -0,0 +1,73 @@
1
+ import chalk from "chalk";
2
+
3
+ // ── Akita brand palette ──────────────────────────────────────────
4
+
5
+ const purple = chalk.hex("#9439e6");
6
+ const pink = chalk.hex("#f35ff2");
7
+ const green = chalk.hex("#44f8bd");
8
+ const gold = chalk.hex("#f5c434");
9
+ const yellow = chalk.hex("#FFEB00");
10
+ const darkBlue = chalk.hex("#0039CB");
11
+ const lightBlue = chalk.hex("#00F0FF");
12
+
13
+ // ── Theme tokens ─────────────────────────────────────────────────
14
+
15
+ const theme = {
16
+ // UI chrome
17
+ border: chalk.dim,
18
+ panelTitle: chalk.bold.hex("#9439e6"),
19
+ appName: chalk.bold.hex("#f35ff2"),
20
+ activeTab: chalk.bgHex("#9439e6").white.bold,
21
+ inactiveTab: chalk.dim,
22
+ tabSeparator: chalk.dim,
23
+ separator: chalk.dim,
24
+ statusBar: chalk.inverse,
25
+ sectionHeader: chalk.bold.hex("#9439e6"),
26
+
27
+ // KV / table labels
28
+ label: chalk.dim,
29
+
30
+ // Selection / navigation
31
+ cursor: pink,
32
+ selected: chalk.bold,
33
+
34
+ // Status colors
35
+ statusApproved: green,
36
+ statusVoting: lightBlue,
37
+ statusDraft: gold,
38
+ statusRejected: chalk.red,
39
+ stateActive: green,
40
+ stateInactive: chalk.red,
41
+ statePaused: gold,
42
+ boolTrue: green,
43
+ boolFalse: chalk.dim,
44
+
45
+ // Proposal action colors
46
+ actionAdd: green,
47
+ actionRemove: chalk.red,
48
+ actionModify: gold,
49
+
50
+ // Escrow status
51
+ locked: chalk.red,
52
+ unlocked: green,
53
+
54
+ // Compact number suffixes
55
+ suffixK: chalk.bold.white,
56
+ suffixM: green,
57
+ suffixB: lightBlue,
58
+ suffixT: purple,
59
+
60
+ // Charts
61
+ barFilled: lightBlue,
62
+ barEmpty: chalk.dim,
63
+ chartLabel: chalk.bold,
64
+ chartDim: chalk.dim,
65
+
66
+ // Revenue split segment palette (cycled)
67
+ splitColors: [purple, pink, green, gold, lightBlue, darkBlue, yellow],
68
+
69
+ // Global caller
70
+ globalCaller: green,
71
+ };
72
+
73
+ export default theme;