@net-protocol/cli 0.1.25 → 0.1.27

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.
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk3 from 'chalk';
3
3
  import { StorageClient, chunkDataForStorage, CHUNKED_STORAGE_CONTRACT } from '@net-protocol/storage';
4
- import { PROFILE_PICTURE_STORAGE_KEY, PROFILE_METADATA_STORAGE_KEY, parseProfileMetadata, PROFILE_CANVAS_STORAGE_KEY, isValidUrl, getProfilePictureStorageArgs, STORAGE_CONTRACT, isValidXUsername, getProfileMetadataStorageArgs, isValidBio, isValidDisplayName, isValidTokenAddress } from '@net-protocol/profiles';
4
+ import { PROFILE_PICTURE_STORAGE_KEY, PROFILE_METADATA_STORAGE_KEY, parseProfileMetadata, PROFILE_CANVAS_STORAGE_KEY, PROFILE_CSS_STORAGE_KEY, isValidUrl, getProfilePictureStorageArgs, STORAGE_CONTRACT, isValidXUsername, getProfileMetadataStorageArgs, isValidBio, isValidDisplayName, isValidTokenAddress, DEMO_THEMES, MAX_CSS_SIZE, isValidCSS, getProfileCSSStorageArgs, buildCSSPrompt } from '@net-protocol/profiles';
5
5
  import { createWalletClient, http, publicActions, encodeFunctionData } from 'viem';
6
6
  import { privateKeyToAccount } from 'viem/accounts';
7
7
  import { base } from 'viem/chains';
@@ -131,7 +131,22 @@ async function executeProfileGet(options) {
131
131
  throw error;
132
132
  }
133
133
  }
134
- const hasProfile = profilePicture || xUsername || bio || tokenAddress || canvasSize;
134
+ let cssSize;
135
+ try {
136
+ const cssResult = await client.readStorageData({
137
+ key: PROFILE_CSS_STORAGE_KEY,
138
+ operator: options.address
139
+ });
140
+ if (cssResult.data) {
141
+ cssSize = cssResult.data.length;
142
+ }
143
+ } catch (error) {
144
+ const errorMessage = error instanceof Error ? error.message : String(error);
145
+ if (errorMessage !== "StoredDataNotFound") {
146
+ throw error;
147
+ }
148
+ }
149
+ const hasProfile = profilePicture || xUsername || bio || tokenAddress || canvasSize || cssSize;
135
150
  if (options.json) {
136
151
  const output = {
137
152
  address: options.address,
@@ -141,6 +156,7 @@ async function executeProfileGet(options) {
141
156
  bio: bio || null,
142
157
  tokenAddress: tokenAddress || null,
143
158
  canvas: canvasSize ? { size: canvasSize, isDataUri: canvasIsDataUri } : null,
159
+ css: cssSize ? { size: cssSize } : null,
144
160
  hasProfile
145
161
  };
146
162
  console.log(JSON.stringify(output, null, 2));
@@ -164,6 +180,9 @@ async function executeProfileGet(options) {
164
180
  console.log(
165
181
  ` ${chalk3.cyan("Canvas:")} ${canvasSize ? `${canvasSize} bytes${canvasIsDataUri ? " (data URI)" : ""}` : chalk3.gray("(not set)")}`
166
182
  );
183
+ console.log(
184
+ ` ${chalk3.cyan("Custom CSS:")} ${cssSize ? `${cssSize} bytes` : chalk3.gray("(not set)")}`
185
+ );
167
186
  if (!hasProfile) {
168
187
  console.log(chalk3.yellow("\n No profile data found for this address."));
169
188
  }
@@ -940,6 +959,197 @@ async function executeProfileGetCanvas(options) {
940
959
  );
941
960
  }
942
961
  }
962
+ async function executeProfileSetCSS(options) {
963
+ const sourceCount = [options.file, options.content, options.theme].filter(
964
+ Boolean
965
+ ).length;
966
+ if (sourceCount === 0) {
967
+ exitWithError(
968
+ "Must provide one of --file, --content, or --theme to set CSS."
969
+ );
970
+ }
971
+ if (sourceCount > 1) {
972
+ exitWithError("Cannot provide more than one of --file, --content, --theme.");
973
+ }
974
+ let cssContent;
975
+ if (options.theme) {
976
+ const theme = DEMO_THEMES[options.theme];
977
+ if (!theme) {
978
+ const available = Object.entries(DEMO_THEMES).map(([key, val]) => ` ${key} \u2014 ${val.name}`).join("\n");
979
+ exitWithError(
980
+ `Unknown theme: "${options.theme}"
981
+
982
+ Available themes:
983
+ ${available}`
984
+ );
985
+ }
986
+ cssContent = theme.css;
987
+ console.log(chalk3.gray(` Using theme: ${theme.name}`));
988
+ } else if (options.file) {
989
+ const filePath = path.resolve(options.file);
990
+ if (!fs.existsSync(filePath)) {
991
+ exitWithError(`File not found: ${filePath}`);
992
+ }
993
+ const buffer = fs.readFileSync(filePath);
994
+ if (buffer.length > MAX_CSS_SIZE) {
995
+ exitWithError(
996
+ `File too large: ${buffer.length} bytes exceeds maximum of ${MAX_CSS_SIZE} bytes (10KB).`
997
+ );
998
+ }
999
+ cssContent = buffer.toString("utf-8");
1000
+ } else {
1001
+ cssContent = options.content;
1002
+ const contentSize = Buffer.byteLength(cssContent, "utf-8");
1003
+ if (contentSize > MAX_CSS_SIZE) {
1004
+ exitWithError(
1005
+ `Content too large: ${contentSize} bytes exceeds maximum of ${MAX_CSS_SIZE} bytes (10KB).`
1006
+ );
1007
+ }
1008
+ }
1009
+ if (!isValidCSS(cssContent)) {
1010
+ exitWithError(
1011
+ "Invalid CSS: content is empty, too large, or contains disallowed patterns (script injection)."
1012
+ );
1013
+ }
1014
+ const storageArgs = getProfileCSSStorageArgs(cssContent);
1015
+ if (options.encodeOnly) {
1016
+ const readOnlyOptions = parseReadOnlyOptions({
1017
+ chainId: options.chainId,
1018
+ rpcUrl: options.rpcUrl
1019
+ });
1020
+ const encoded = encodeTransaction(
1021
+ {
1022
+ to: STORAGE_CONTRACT.address,
1023
+ abi: STORAGE_CONTRACT.abi,
1024
+ functionName: "put",
1025
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
1026
+ },
1027
+ readOnlyOptions.chainId
1028
+ );
1029
+ console.log(JSON.stringify(encoded, null, 2));
1030
+ return;
1031
+ }
1032
+ const commonOptions = parseCommonOptions(
1033
+ {
1034
+ privateKey: options.privateKey,
1035
+ chainId: options.chainId,
1036
+ rpcUrl: options.rpcUrl
1037
+ },
1038
+ true
1039
+ );
1040
+ try {
1041
+ const account = privateKeyToAccount(commonOptions.privateKey);
1042
+ const rpcUrls = getChainRpcUrls({
1043
+ chainId: commonOptions.chainId,
1044
+ rpcUrl: commonOptions.rpcUrl
1045
+ });
1046
+ const client = createWalletClient({
1047
+ account,
1048
+ chain: base,
1049
+ transport: http(rpcUrls[0])
1050
+ }).extend(publicActions);
1051
+ console.log(chalk3.blue(`Setting profile CSS...`));
1052
+ console.log(
1053
+ chalk3.gray(
1054
+ ` Content size: ${Buffer.byteLength(cssContent, "utf-8")} bytes`
1055
+ )
1056
+ );
1057
+ console.log(chalk3.gray(` Address: ${account.address}`));
1058
+ const hash = await client.writeContract({
1059
+ address: STORAGE_CONTRACT.address,
1060
+ abi: STORAGE_CONTRACT.abi,
1061
+ functionName: "put",
1062
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
1063
+ });
1064
+ console.log(chalk3.blue(`Waiting for confirmation...`));
1065
+ const receipt = await client.waitForTransactionReceipt({ hash });
1066
+ if (receipt.status === "success") {
1067
+ console.log(
1068
+ chalk3.green(
1069
+ `
1070
+ CSS updated successfully!
1071
+ Transaction: ${hash}
1072
+ Content size: ${Buffer.byteLength(cssContent, "utf-8")} bytes`
1073
+ )
1074
+ );
1075
+ } else {
1076
+ exitWithError(`Transaction failed: ${hash}`);
1077
+ }
1078
+ } catch (error) {
1079
+ exitWithError(
1080
+ `Failed to set CSS: ${error instanceof Error ? error.message : String(error)}`
1081
+ );
1082
+ }
1083
+ }
1084
+ async function executeProfileGetCSS(options) {
1085
+ const readOnlyOptions = parseReadOnlyOptions({
1086
+ chainId: options.chainId,
1087
+ rpcUrl: options.rpcUrl
1088
+ });
1089
+ const client = new StorageClient({
1090
+ chainId: readOnlyOptions.chainId,
1091
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
1092
+ });
1093
+ try {
1094
+ let cssContent;
1095
+ try {
1096
+ const result = await client.readStorageData({
1097
+ key: PROFILE_CSS_STORAGE_KEY,
1098
+ operator: options.address
1099
+ });
1100
+ if (result.data) {
1101
+ cssContent = result.data;
1102
+ }
1103
+ } catch (error) {
1104
+ const errorMessage = error instanceof Error ? error.message : String(error);
1105
+ if (errorMessage !== "StoredDataNotFound") {
1106
+ throw error;
1107
+ }
1108
+ }
1109
+ if (options.json) {
1110
+ const output = {
1111
+ address: options.address,
1112
+ chainId: readOnlyOptions.chainId,
1113
+ css: cssContent || null,
1114
+ hasCSS: !!cssContent,
1115
+ contentLength: cssContent ? cssContent.length : 0
1116
+ };
1117
+ console.log(JSON.stringify(output, null, 2));
1118
+ return;
1119
+ }
1120
+ if (!cssContent) {
1121
+ exitWithError(`No custom CSS found for address: ${options.address}`);
1122
+ }
1123
+ if (options.output) {
1124
+ const outputPath = path.resolve(options.output);
1125
+ fs.writeFileSync(outputPath, cssContent, "utf-8");
1126
+ console.log(
1127
+ chalk3.green(
1128
+ `CSS written to: ${outputPath} (${cssContent.length} bytes)`
1129
+ )
1130
+ );
1131
+ return;
1132
+ }
1133
+ console.log(cssContent);
1134
+ } catch (error) {
1135
+ exitWithError(
1136
+ `Failed to read CSS: ${error instanceof Error ? error.message : String(error)}`
1137
+ );
1138
+ }
1139
+ }
1140
+ async function executeProfileCSSPrompt(options) {
1141
+ if (options.listThemes) {
1142
+ console.log("Available demo themes:\n");
1143
+ for (const [key, theme] of Object.entries(DEMO_THEMES)) {
1144
+ console.log(` ${key} \u2014 ${theme.name}`);
1145
+ }
1146
+ console.log(
1147
+ "\nUse with: net profile set-css --theme <name>"
1148
+ );
1149
+ return;
1150
+ }
1151
+ console.log(buildCSSPrompt());
1152
+ }
943
1153
 
944
1154
  // src/commands/profile/index.ts
945
1155
  function registerProfileCommand(program) {
@@ -1130,6 +1340,51 @@ function registerProfileCommand(program) {
1130
1340
  json: options.json
1131
1341
  });
1132
1342
  });
1343
+ const setCSSCommand = new Command("set-css").description("Set your profile custom CSS theme").option("--file <path>", "Path to CSS file").option("--content <css>", "CSS content (inline)").option("--theme <name>", "Use a built-in demo theme (e.g. hotPink, midnightGrunge, ocean)").option(
1344
+ "--private-key <key>",
1345
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
1346
+ ).option(
1347
+ "--chain-id <id>",
1348
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
1349
+ (value) => parseInt(value, 10)
1350
+ ).option(
1351
+ "--rpc-url <url>",
1352
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
1353
+ ).option(
1354
+ "--encode-only",
1355
+ "Output transaction data as JSON instead of executing"
1356
+ ).action(async (options) => {
1357
+ await executeProfileSetCSS({
1358
+ file: options.file,
1359
+ content: options.content,
1360
+ theme: options.theme,
1361
+ privateKey: options.privateKey,
1362
+ chainId: options.chainId,
1363
+ rpcUrl: options.rpcUrl,
1364
+ encodeOnly: options.encodeOnly
1365
+ });
1366
+ });
1367
+ const getCSSCommand = new Command("get-css").description("Get profile custom CSS for an address").requiredOption("--address <address>", "Wallet address to get CSS for").option("--output <path>", "Write CSS content to file instead of stdout").option(
1368
+ "--chain-id <id>",
1369
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
1370
+ (value) => parseInt(value, 10)
1371
+ ).option(
1372
+ "--rpc-url <url>",
1373
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
1374
+ ).option("--json", "Output in JSON format").action(async (options) => {
1375
+ await executeProfileGetCSS({
1376
+ address: options.address,
1377
+ output: options.output,
1378
+ chainId: options.chainId,
1379
+ rpcUrl: options.rpcUrl,
1380
+ json: options.json
1381
+ });
1382
+ });
1383
+ const cssPromptCommand = new Command("css-prompt").description("Print the AI prompt for generating profile CSS themes").option("--list-themes", "List available demo themes instead of the prompt").action(async (options) => {
1384
+ await executeProfileCSSPrompt({
1385
+ listThemes: options.listThemes
1386
+ });
1387
+ });
1133
1388
  profileCommand.addCommand(getCommand);
1134
1389
  profileCommand.addCommand(setPictureCommand);
1135
1390
  profileCommand.addCommand(setUsernameCommand);
@@ -1138,6 +1393,9 @@ function registerProfileCommand(program) {
1138
1393
  profileCommand.addCommand(setTokenAddressCommand);
1139
1394
  profileCommand.addCommand(setCanvasCommand);
1140
1395
  profileCommand.addCommand(getCanvasCommand);
1396
+ profileCommand.addCommand(setCSSCommand);
1397
+ profileCommand.addCommand(getCSSCommand);
1398
+ profileCommand.addCommand(cssPromptCommand);
1141
1399
  }
1142
1400
 
1143
1401
  export { registerProfileCommand };