@andrzejchm/notion-cli 0.3.0 → 0.4.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/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync } from "fs";
5
5
  import { dirname, join as join3 } from "path";
6
6
  import { fileURLToPath } from "url";
7
- import { Command as Command19 } from "commander";
7
+ import { Command as Command20 } from "commander";
8
8
 
9
9
  // src/commands/append.ts
10
10
  import { Command } from "commander";
@@ -692,26 +692,26 @@ function createNotionClient(token) {
692
692
  var NOTION_ID_REGEX = /^[0-9a-f]{32}$/i;
693
693
  var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
694
694
  var NOTION_URL_REGEX = /https?:\/\/(?:[a-zA-Z0-9-]+\.)?notion\.(?:so|site)\/.*?([0-9a-f]{32})(?:[?#]|$)/i;
695
- function throwInvalidId(input2) {
695
+ function throwInvalidId(input3) {
696
696
  throw new CliError(
697
697
  ErrorCodes.INVALID_ID,
698
- `Cannot parse Notion ID from: ${input2}`,
698
+ `Cannot parse Notion ID from: ${input3}`,
699
699
  "Provide a valid Notion URL or page/database ID"
700
700
  );
701
701
  }
702
- function parseNotionId(input2) {
703
- if (!input2) throwInvalidId(input2);
704
- if (NOTION_ID_REGEX.test(input2)) {
705
- return input2.toLowerCase();
702
+ function parseNotionId(input3) {
703
+ if (!input3) throwInvalidId(input3);
704
+ if (NOTION_ID_REGEX.test(input3)) {
705
+ return input3.toLowerCase();
706
706
  }
707
- if (UUID_REGEX.test(input2)) {
708
- return input2.replace(/-/g, "").toLowerCase();
707
+ if (UUID_REGEX.test(input3)) {
708
+ return input3.replace(/-/g, "").toLowerCase();
709
709
  }
710
- const urlMatch = NOTION_URL_REGEX.exec(input2);
710
+ const urlMatch = NOTION_URL_REGEX.exec(input3);
711
711
  if (urlMatch) {
712
712
  return urlMatch[1].toLowerCase();
713
713
  }
714
- throwInvalidId(input2);
714
+ throwInvalidId(input3);
715
715
  }
716
716
  function toUuid(id) {
717
717
  return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
@@ -825,7 +825,7 @@ function authDefaultAction(authCmd2) {
825
825
  }
826
826
 
827
827
  // src/commands/auth/login.ts
828
- import { select } from "@inquirer/prompts";
828
+ import { input as input2, select } from "@inquirer/prompts";
829
829
  import { Command as Command3 } from "commander";
830
830
 
831
831
  // src/oauth/oauth-flow.ts
@@ -924,6 +924,13 @@ Paste the full redirect URL here (${OAUTH_REDIRECT_URI}?code=...):
924
924
  });
925
925
  });
926
926
  }
927
+ function closeServer(ctx, cb) {
928
+ for (const socket of ctx.sockets) {
929
+ socket.destroy();
930
+ }
931
+ ctx.sockets.clear();
932
+ ctx.server.close(cb);
933
+ }
927
934
  function handleCallbackRequest(req, res, ctx) {
928
935
  if (ctx.settled) {
929
936
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -944,7 +951,7 @@ function handleCallbackRequest(req, res, ctx) {
944
951
  "<html><body><h1>Access Denied</h1><p>You cancelled the Notion OAuth request. You can close this tab.</p></body></html>"
945
952
  );
946
953
  if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
947
- ctx.server.close(() => {
954
+ closeServer(ctx, () => {
948
955
  ctx.reject(
949
956
  new CliError(
950
957
  ErrorCodes.AUTH_INVALID,
@@ -967,7 +974,7 @@ function handleCallbackRequest(req, res, ctx) {
967
974
  "<html><body><h1>Security Error</h1><p>State mismatch \u2014 possible CSRF attempt. You can close this tab.</p></body></html>"
968
975
  );
969
976
  if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
970
- ctx.server.close(() => {
977
+ closeServer(ctx, () => {
971
978
  ctx.reject(
972
979
  new CliError(
973
980
  ErrorCodes.AUTH_INVALID,
@@ -984,7 +991,7 @@ function handleCallbackRequest(req, res, ctx) {
984
991
  "<html><body><h1>Authenticated!</h1><p>You can close this tab and return to the terminal.</p></body></html>"
985
992
  );
986
993
  if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
987
- ctx.server.close(() => {
994
+ closeServer(ctx, () => {
988
995
  ctx.resolve({ code, state: returnedState });
989
996
  });
990
997
  } catch {
@@ -1006,12 +1013,17 @@ async function runOAuthFlow(options) {
1006
1013
  resolve,
1007
1014
  reject,
1008
1015
  // assigned below after server is created
1009
- server: null
1016
+ server: null,
1017
+ sockets: /* @__PURE__ */ new Set()
1010
1018
  };
1011
1019
  const server = createServer(
1012
1020
  (req, res) => handleCallbackRequest(req, res, ctx)
1013
1021
  );
1014
1022
  ctx.server = server;
1023
+ server.on("connection", (socket) => {
1024
+ ctx.sockets.add(socket);
1025
+ socket.once("close", () => ctx.sockets.delete(socket));
1026
+ });
1015
1027
  server.on("error", (err) => {
1016
1028
  if (ctx.settled) return;
1017
1029
  ctx.settled = true;
@@ -1045,7 +1057,7 @@ Waiting for callback (up to 120 seconds)...
1045
1057
  ctx.timeoutHandle = setTimeout(() => {
1046
1058
  if (ctx.settled) return;
1047
1059
  ctx.settled = true;
1048
- server.close(() => {
1060
+ closeServer(ctx, () => {
1049
1061
  reject(
1050
1062
  new CliError(
1051
1063
  ErrorCodes.AUTH_INVALID,
@@ -1188,19 +1200,44 @@ function loginCommand() {
1188
1200
  ]
1189
1201
  });
1190
1202
  if (method === "oauth") {
1191
- let profileName = opts.profile;
1192
- if (!profileName) {
1193
- const config = await readGlobalConfig();
1194
- profileName = config.active_profile ?? "default";
1195
- }
1196
1203
  const result = await runOAuthFlow({ manual: opts.manual });
1197
1204
  const response = await exchangeCode(result.code);
1198
- await saveOAuthTokens(profileName, response);
1199
1205
  const userName = response.owner?.user?.name ?? "unknown user";
1200
1206
  const workspaceName = response.workspace_name ?? "unknown workspace";
1207
+ const config = await readGlobalConfig();
1208
+ const existingProfiles = config.profiles ?? {};
1209
+ let profileName = opts.profile;
1210
+ if (!profileName) {
1211
+ const suggested = workspaceName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "default";
1212
+ profileName = await input2({
1213
+ message: "Profile name to save this account under:",
1214
+ default: suggested
1215
+ });
1216
+ }
1217
+ const isUpdate = Boolean(existingProfiles[profileName]);
1218
+ const isFirst = Object.keys(existingProfiles).length === 0;
1219
+ if (isUpdate) {
1220
+ stderrWrite(dim(`Updating existing profile "${profileName}"...`));
1221
+ }
1222
+ await saveOAuthTokens(profileName, response);
1223
+ if (isFirst) {
1224
+ const updated = await readGlobalConfig();
1225
+ await writeGlobalConfig({
1226
+ ...updated,
1227
+ active_profile: profileName
1228
+ });
1229
+ }
1201
1230
  stderrWrite(
1202
1231
  success(`\u2713 Logged in as ${userName} to workspace ${workspaceName}`)
1203
1232
  );
1233
+ stderrWrite(dim(`Saved as profile "${profileName}".`));
1234
+ if (!isFirst && !isUpdate) {
1235
+ stderrWrite(
1236
+ dim(
1237
+ `Run "notion auth use ${profileName}" to switch to this profile.`
1238
+ )
1239
+ );
1240
+ }
1204
1241
  stderrWrite(
1205
1242
  dim(
1206
1243
  "Your comments and pages will now be attributed to your Notion account."
@@ -2053,10 +2090,38 @@ function profileListCommand() {
2053
2090
  return cmd;
2054
2091
  }
2055
2092
 
2056
- // src/commands/profile/use.ts
2093
+ // src/commands/profile/remove.ts
2057
2094
  import { Command as Command15 } from "commander";
2095
+ function profileRemoveCommand() {
2096
+ const cmd = new Command15("remove");
2097
+ cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(
2098
+ withErrorHandling(async (name) => {
2099
+ const config = await readGlobalConfig();
2100
+ const profiles = { ...config.profiles ?? {} };
2101
+ if (!profiles[name]) {
2102
+ throw new CliError(
2103
+ ErrorCodes.AUTH_PROFILE_NOT_FOUND,
2104
+ `Profile "${name}" not found.`,
2105
+ `Run "notion auth list" to see available profiles`
2106
+ );
2107
+ }
2108
+ delete profiles[name];
2109
+ const newActiveProfile = config.active_profile === name ? void 0 : config.active_profile;
2110
+ await writeGlobalConfig({
2111
+ ...config,
2112
+ profiles,
2113
+ active_profile: newActiveProfile
2114
+ });
2115
+ stderrWrite(success(`Profile "${name}" removed.`));
2116
+ })
2117
+ );
2118
+ return cmd;
2119
+ }
2120
+
2121
+ // src/commands/profile/use.ts
2122
+ import { Command as Command16 } from "commander";
2058
2123
  function profileUseCommand() {
2059
- const cmd = new Command15("use");
2124
+ const cmd = new Command16("use");
2060
2125
  cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
2061
2126
  withErrorHandling(async (name) => {
2062
2127
  const config = await readGlobalConfig();
@@ -2079,7 +2144,7 @@ function profileUseCommand() {
2079
2144
  }
2080
2145
 
2081
2146
  // src/commands/read.ts
2082
- import { Command as Command16 } from "commander";
2147
+ import { Command as Command17 } from "commander";
2083
2148
 
2084
2149
  // src/blocks/rich-text.ts
2085
2150
  function richTextToMd(richText) {
@@ -2501,7 +2566,7 @@ async function fetchPageWithBlocks(client, pageId) {
2501
2566
 
2502
2567
  // src/commands/read.ts
2503
2568
  function readCommand() {
2504
- return new Command16("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").option("--json", "Output raw JSON instead of markdown").option("--md", "Output raw markdown (no terminal styling)").action(
2569
+ return new Command17("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").option("--json", "Output raw JSON instead of markdown").option("--md", "Output raw markdown (no terminal styling)").action(
2505
2570
  withErrorHandling(
2506
2571
  async (id, options) => {
2507
2572
  const { token } = await resolveToken();
@@ -2528,7 +2593,7 @@ function readCommand() {
2528
2593
 
2529
2594
  // src/commands/search.ts
2530
2595
  import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
2531
- import { Command as Command17 } from "commander";
2596
+ import { Command as Command18 } from "commander";
2532
2597
  function getTitle2(item) {
2533
2598
  if (item.object === "data_source") {
2534
2599
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -2548,7 +2613,7 @@ function displayType2(item) {
2548
2613
  return item.object === "data_source" ? "database" : item.object;
2549
2614
  }
2550
2615
  function searchCommand() {
2551
- const cmd = new Command17("search");
2616
+ const cmd = new Command18("search");
2552
2617
  cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
2553
2618
  "--type <type>",
2554
2619
  "filter by object type (page or database)",
@@ -2606,7 +2671,7 @@ function searchCommand() {
2606
2671
  }
2607
2672
 
2608
2673
  // src/commands/users.ts
2609
- import { Command as Command18 } from "commander";
2674
+ import { Command as Command19 } from "commander";
2610
2675
  function getEmailOrWorkspace(user) {
2611
2676
  if (user.type === "person") {
2612
2677
  return user.person.email ?? "\u2014";
@@ -2618,7 +2683,7 @@ function getEmailOrWorkspace(user) {
2618
2683
  return "\u2014";
2619
2684
  }
2620
2685
  function usersCommand() {
2621
- const cmd = new Command18("users");
2686
+ const cmd = new Command19("users");
2622
2687
  cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
2623
2688
  withErrorHandling(async (opts) => {
2624
2689
  if (opts.json) setOutputMode("json");
@@ -2649,7 +2714,7 @@ var __dirname = dirname(__filename);
2649
2714
  var pkg = JSON.parse(
2650
2715
  readFileSync(join3(__dirname, "../package.json"), "utf-8")
2651
2716
  );
2652
- var program = new Command19();
2717
+ var program = new Command20();
2653
2718
  program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
2654
2719
  program.option("--verbose", "show API requests/responses").option("--color", "force color output").option("--json", "force JSON output (overrides TTY detection)").option("--md", "force markdown output for page content");
2655
2720
  program.configureOutput({
@@ -2670,20 +2735,22 @@ program.hook("preAction", (thisCommand) => {
2670
2735
  setOutputMode("md");
2671
2736
  }
2672
2737
  });
2673
- var authCmd = new Command19("auth").description("manage Notion authentication");
2738
+ var authCmd = new Command20("auth").description("manage Notion authentication");
2674
2739
  authCmd.action(authDefaultAction(authCmd));
2675
2740
  authCmd.addCommand(loginCommand());
2676
2741
  authCmd.addCommand(logoutCommand());
2677
2742
  authCmd.addCommand(statusCommand());
2678
2743
  authCmd.addCommand(profileListCommand());
2679
2744
  authCmd.addCommand(profileUseCommand());
2745
+ authCmd.addCommand(profileRemoveCommand());
2680
2746
  program.addCommand(authCmd);
2681
2747
  program.addCommand(initCommand(), { hidden: true });
2682
- var profileCmd = new Command19("profile").description(
2748
+ var profileCmd = new Command20("profile").description(
2683
2749
  "manage authentication profiles"
2684
2750
  );
2685
2751
  profileCmd.addCommand(profileListCommand());
2686
2752
  profileCmd.addCommand(profileUseCommand());
2753
+ profileCmd.addCommand(profileRemoveCommand());
2687
2754
  program.addCommand(profileCmd, { hidden: true });
2688
2755
  program.addCommand(searchCommand());
2689
2756
  program.addCommand(lsCommand());
@@ -2694,7 +2761,7 @@ program.addCommand(readCommand());
2694
2761
  program.addCommand(commentAddCommand());
2695
2762
  program.addCommand(appendCommand());
2696
2763
  program.addCommand(createPageCommand());
2697
- var dbCmd = new Command19("db").description("Database operations");
2764
+ var dbCmd = new Command20("db").description("Database operations");
2698
2765
  dbCmd.addCommand(dbSchemaCommand());
2699
2766
  dbCmd.addCommand(dbQueryCommand());
2700
2767
  program.addCommand(dbCmd);