create-kanojo 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/dist/index.mjs +600 -112
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -9318,21 +9318,31 @@ function createNostrService(options = {}) {
9318
9318
  };
9319
9319
  }
9320
9320
  async function closeListing(profile, listingId) {
9321
- const listing = profile.cache.listings.find((item) => item.id === listingId);
9322
- if (!listing) throw new Error(`Listing not found: ${listingId}`);
9323
- const closedListing = {
9321
+ return updateListing(profile, {
9322
+ listingId,
9323
+ status: "closed"
9324
+ });
9325
+ }
9326
+ async function updateListing(profile, input) {
9327
+ const listing = profile.cache.listings.find((item) => item.id === input.listingId);
9328
+ if (!listing) throw new Error(`Listing not found: ${input.listingId}`);
9329
+ const nextListing = {
9324
9330
  ...listing,
9325
- status: "closed",
9331
+ headline: input.headline?.trim() ?? listing.headline,
9332
+ summary: input.summary?.trim() ?? listing.summary,
9333
+ region: input.region?.trim() ?? listing.region,
9334
+ desiredTags: uniqueStrings(input.desiredTags ?? listing.desiredTags),
9335
+ status: input.status ?? listing.status,
9326
9336
  updatedAt: now()
9327
9337
  };
9328
9338
  const secretKey = decodeNsec(profile.nostr.nsec);
9329
- const signed = finalizeEvent(serializeMatchingListingEvent(profile, closedListing), secretKey);
9339
+ const signed = finalizeEvent(serializeMatchingListingEvent(profile, nextListing), secretKey);
9330
9340
  await transport.publish(profile.relays, [signed]);
9331
9341
  return {
9332
9342
  ...profile,
9333
9343
  cache: {
9334
9344
  ...profile.cache,
9335
- listings: [closedListing, ...profile.cache.listings.filter((item) => item.id !== closedListing.id)],
9345
+ listings: [nextListing, ...profile.cache.listings.filter((item) => item.id !== nextListing.id)],
9336
9346
  lastListingSyncAt: now()
9337
9347
  }
9338
9348
  };
@@ -9461,6 +9471,7 @@ function createNostrService(options = {}) {
9461
9471
  publishListing,
9462
9472
  refreshOwnListings,
9463
9473
  closeListing,
9474
+ updateListing,
9464
9475
  discoverListings,
9465
9476
  syncInbox,
9466
9477
  sendLike,
@@ -9478,6 +9489,13 @@ function createGeneratedCredentials() {
9478
9489
  nsec: nip19_exports.nsecEncode(secretKey)
9479
9490
  };
9480
9491
  }
9492
+ function importCredentials(nsec) {
9493
+ const secretKey = decodeNsec(nsec.trim());
9494
+ return {
9495
+ pubkey: getPublicKey(secretKey),
9496
+ nsec: nip19_exports.nsecEncode(secretKey)
9497
+ };
9498
+ }
9481
9499
  function createSimplePoolTransport() {
9482
9500
  const pool = new SimplePool();
9483
9501
  return {
@@ -9625,29 +9643,85 @@ function createMatchingCli(preset, options = {}) {
9625
9643
  const store = loadProfileStore(options.baseDir);
9626
9644
  const service = createNostrService(options);
9627
9645
  const theme = createTheme(preset);
9646
+ let plainOutput = false;
9628
9647
  return { async run(rawArgs = process.argv.slice(2)) {
9629
9648
  await store.ensure();
9630
- Wt(theme.banner(` ${preset.brand} `));
9631
9649
  try {
9632
9650
  const parsed = extractProfileOverride(rawArgs);
9633
9651
  const command = parseCommandFlags(parsed.args);
9634
9652
  const [rootCommand] = command.positionals;
9653
+ plainOutput = command.positionals.length > 0 || Boolean(command.flags.help) || !process.stdout.isTTY;
9654
+ showIntro();
9635
9655
  if (command.flags.help || rootCommand === "help") {
9636
- Vt(renderCliUsage(preset), "Usage");
9656
+ showText(renderCliUsage(preset));
9637
9657
  return;
9638
9658
  }
9639
9659
  await dispatchCommand(command, parsed.profileName);
9640
- Gt(theme.accent("Connected quietly. Ready for the next good match."));
9660
+ showOutro("Connected quietly. Ready for the next good match.");
9641
9661
  } catch (error) {
9642
9662
  if (error instanceof CancelledFlowError) {
9643
- Nt("Operation cancelled.");
9663
+ showCancelled("Operation cancelled.");
9644
9664
  return;
9645
9665
  }
9646
- R.error(error instanceof Error ? error.message : "Unexpected error.");
9666
+ showError(error instanceof Error ? error.message : "Unexpected error.");
9647
9667
  } finally {
9648
9668
  service.close();
9649
9669
  }
9650
9670
  } };
9671
+ function showIntro() {
9672
+ if (!plainOutput) Wt(theme.banner(` ${preset.brand} `));
9673
+ }
9674
+ function showOutro(message) {
9675
+ if (!plainOutput) Gt(theme.accent(message));
9676
+ }
9677
+ function showCancelled(message) {
9678
+ if (plainOutput) {
9679
+ process.stderr.write(`${message}\n`);
9680
+ return;
9681
+ }
9682
+ Nt(message);
9683
+ }
9684
+ function showText(message) {
9685
+ process.stdout.write(`${message}\n`);
9686
+ }
9687
+ function showSection(body, title) {
9688
+ if (plainOutput) {
9689
+ showText(`${title}\n${body}`);
9690
+ return;
9691
+ }
9692
+ Vt(body, title);
9693
+ }
9694
+ function showInfo(message) {
9695
+ if (plainOutput) {
9696
+ showText(message);
9697
+ return;
9698
+ }
9699
+ R.info(message);
9700
+ }
9701
+ function showSuccess(message) {
9702
+ if (plainOutput) {
9703
+ showText(message);
9704
+ return;
9705
+ }
9706
+ R.success(message);
9707
+ }
9708
+ function showStep(message) {
9709
+ if (plainOutput) {
9710
+ showText(message);
9711
+ return;
9712
+ }
9713
+ R.step(message);
9714
+ }
9715
+ function showError(message) {
9716
+ if (plainOutput) {
9717
+ process.stderr.write(`Error: ${message}\n`);
9718
+ return;
9719
+ }
9720
+ R.error(message);
9721
+ }
9722
+ async function runWithSpinner(message, task) {
9723
+ return withSpinner(message, task, plainOutput);
9724
+ }
9651
9725
  async function dispatchCommand(parsedArgs, profileOverride) {
9652
9726
  const [command, subcommand, ...rest] = parsedArgs.positionals;
9653
9727
  if (!command) {
@@ -9663,7 +9737,7 @@ function createMatchingCli(preset, options = {}) {
9663
9737
  return;
9664
9738
  }
9665
9739
  if (command === "discover") {
9666
- await runDiscover(profileOverride);
9740
+ await runDiscoverCommand(subcommand, rest, parsedArgs.flags, profileOverride);
9667
9741
  return;
9668
9742
  }
9669
9743
  if (command === "likes") {
@@ -9675,13 +9749,21 @@ function createMatchingCli(preset, options = {}) {
9675
9749
  return;
9676
9750
  }
9677
9751
  if (command === "chat") {
9678
- await runChat(profileOverride, subcommand, parsedArgs.flags);
9752
+ await runChat(profileOverride, subcommand, rest, parsedArgs.flags);
9679
9753
  return;
9680
9754
  }
9681
9755
  if (command === "config") {
9682
9756
  await runConfigCommand(subcommand, parsedArgs.flags, profileOverride);
9683
9757
  return;
9684
9758
  }
9759
+ if (command === "inbox") {
9760
+ await runInbox(profileOverride);
9761
+ return;
9762
+ }
9763
+ if (command === "watch") {
9764
+ await runWatch(profileOverride, parsedArgs.flags);
9765
+ return;
9766
+ }
9685
9767
  throw new Error(`Unknown command: ${command}`);
9686
9768
  }
9687
9769
  async function runHome(profileOverride) {
@@ -9690,7 +9772,7 @@ function createMatchingCli(preset, options = {}) {
9690
9772
  profile = await service.syncInbox(profile);
9691
9773
  await store.saveProfile(profile);
9692
9774
  await store.setActiveProfile(preset.brand, profile.profileName);
9693
- Vt(renderProfileCard(profile), "Current Profile");
9775
+ showSection(renderProfileCard(profile), "Current Profile");
9694
9776
  const action = await askSelect({
9695
9777
  message: "What do you want to do next?",
9696
9778
  options: [
@@ -9753,7 +9835,7 @@ function createMatchingCli(preset, options = {}) {
9753
9835
  continue;
9754
9836
  }
9755
9837
  if (action === "profile-show") {
9756
- Vt(renderProfileCard(profile, true), "Profile Details");
9838
+ showSection(renderProfileCard(profile, true), "Profile Details");
9757
9839
  continue;
9758
9840
  }
9759
9841
  if (action === "switch-profile") {
@@ -9783,6 +9865,23 @@ function createMatchingCli(preset, options = {}) {
9783
9865
  });
9784
9866
  return;
9785
9867
  }
9868
+ if (subcommand === "import") {
9869
+ await promptProfileImport({
9870
+ profileName: getStringFlag(flags, "name", "profile-name"),
9871
+ displayName: getStringFlag(flags, "display-name"),
9872
+ ageRange: getStringFlag(flags, "age-range"),
9873
+ region: getStringFlag(flags, "region"),
9874
+ bio: getStringFlag(flags, "bio"),
9875
+ interests: getStringFlag(flags, "interests"),
9876
+ lookingForAge: getStringFlag(flags, "looking-age", "looking-age-range"),
9877
+ lookingForRegions: getStringFlag(flags, "looking-regions"),
9878
+ lookingForNotes: getStringFlag(flags, "looking-notes"),
9879
+ relays: getStringFlag(flags, "relays"),
9880
+ nsec: getStringFlag(flags, "nsec"),
9881
+ publish: Boolean(flags.publish)
9882
+ });
9883
+ return;
9884
+ }
9786
9885
  if (subcommand === "use") {
9787
9886
  await promptProfileUse(args[0] ?? profileOverride);
9788
9887
  return;
@@ -9791,17 +9890,30 @@ function createMatchingCli(preset, options = {}) {
9791
9890
  const profiles = await store.listProfiles();
9792
9891
  const active = await store.getActiveProfileName(preset.brand);
9793
9892
  if (profiles.length === 0) {
9794
- R.info("No profiles yet. Start with `profile create`.");
9893
+ showInfo("No profiles yet. Start with `profile create`.");
9795
9894
  return;
9796
9895
  }
9797
- Vt(profiles.map((name) => `${name === active ? "●" : "○"} ${name}`).join("\n"), "Profiles");
9896
+ showSection(profiles.map((name) => `${name === active ? "●" : "○"} ${name}`).join("\n"), "Profiles");
9798
9897
  return;
9799
9898
  }
9800
9899
  if (subcommand === "show") {
9801
- Vt(renderProfileCard(await ensureProfile(profileOverride), true), "Profile Details");
9900
+ showSection(renderProfileCard(await ensureProfile(profileOverride), true), "Profile Details");
9802
9901
  return;
9803
9902
  }
9804
- throw new Error("Use `profile create|use|list|show`.");
9903
+ if (subcommand === "edit") {
9904
+ await promptProfileEdit(await ensureProfile(profileOverride), {
9905
+ displayName: getStringFlag(flags, "display-name"),
9906
+ ageRange: getStringFlag(flags, "age-range"),
9907
+ region: getStringFlag(flags, "region"),
9908
+ bio: getStringFlag(flags, "bio"),
9909
+ interests: getStringFlag(flags, "interests"),
9910
+ lookingForAge: getStringFlag(flags, "looking-age", "looking-age-range"),
9911
+ lookingForRegions: getStringFlag(flags, "looking-regions"),
9912
+ lookingForNotes: getStringFlag(flags, "looking-notes")
9913
+ });
9914
+ return;
9915
+ }
9916
+ throw new Error("Use `profile create|import|edit|use|list|show`.");
9805
9917
  }
9806
9918
  async function runListingCommand(subcommand, _args, flags, profileOverride) {
9807
9919
  const profile = await ensureProfile(profileOverride);
@@ -9817,14 +9929,14 @@ function createMatchingCli(preset, options = {}) {
9817
9929
  if (subcommand === "list") {
9818
9930
  const refreshed = await service.refreshOwnListings(profile);
9819
9931
  await store.saveProfile(refreshed);
9820
- Vt(renderListings(refreshed.cache.listings), "Your Listings");
9932
+ showSection(renderListings(refreshed.cache.listings), "Your Listings");
9821
9933
  return;
9822
9934
  }
9823
9935
  if (subcommand === "close") {
9824
9936
  const refreshed = await service.refreshOwnListings(profile);
9825
9937
  const openListings = refreshed.cache.listings.filter((listing) => listing.status === "open");
9826
9938
  if (openListings.length === 0) {
9827
- R.info("There are no open listings to close.");
9939
+ showInfo("There are no open listings to close.");
9828
9940
  return;
9829
9941
  }
9830
9942
  const listingIdArg = getStringFlag(flags, "id", "listing-id");
@@ -9839,15 +9951,50 @@ function createMatchingCli(preset, options = {}) {
9839
9951
  hint: listing.region
9840
9952
  }))
9841
9953
  });
9842
- const nextProfile = await withSpinner("Closing listing...", () => service.closeListing(refreshed, listingId));
9954
+ const nextProfile = await runWithSpinner("Closing listing...", () => service.closeListing(refreshed, listingId));
9843
9955
  await store.saveProfile(nextProfile);
9844
- R.success("Listing closed.");
9956
+ showSuccess("Listing closed.");
9957
+ return;
9958
+ }
9959
+ if (subcommand === "edit") {
9960
+ const refreshed = await service.refreshOwnListings(profile);
9961
+ await store.saveProfile(refreshed);
9962
+ await promptListingEdit(refreshed, {
9963
+ listingRef: getStringFlag(flags, "id", "listing-id", "address", "listing-address") ?? _args[0],
9964
+ headline: getStringFlag(flags, "title", "headline"),
9965
+ summary: getStringFlag(flags, "summary"),
9966
+ region: getStringFlag(flags, "region"),
9967
+ desiredTags: getStringFlag(flags, "tags", "desired-tags")
9968
+ });
9845
9969
  return;
9846
9970
  }
9847
- throw new Error("Use `listing publish|list|close`.");
9971
+ if (subcommand === "reopen") {
9972
+ const refreshed = await service.refreshOwnListings(profile);
9973
+ await store.saveProfile(refreshed);
9974
+ await reopenListing(refreshed, getStringFlag(flags, "id", "listing-id", "address", "listing-address") ?? _args[0]);
9975
+ return;
9976
+ }
9977
+ throw new Error("Use `listing publish|list|close|edit|reopen`.");
9848
9978
  }
9849
- async function runDiscover(profileOverride) {
9850
- await handleDiscover(await ensureProfile(profileOverride));
9979
+ async function runDiscoverCommand(subcommand, args, flags, profileOverride) {
9980
+ const profile = await ensureProfile(profileOverride);
9981
+ if (!subcommand) {
9982
+ await handleDiscover(profile);
9983
+ return;
9984
+ }
9985
+ if (subcommand === "list") {
9986
+ await showDiscoverList(profile);
9987
+ return;
9988
+ }
9989
+ if (subcommand === "like") {
9990
+ await likeDiscoveredListing(profile, args[0] ?? getStringFlag(flags, "id", "listing", "address"), flags);
9991
+ return;
9992
+ }
9993
+ if (subcommand === "pass") {
9994
+ await passDiscoveredListing(profile, args[0] ?? getStringFlag(flags, "id", "listing", "address"));
9995
+ return;
9996
+ }
9997
+ throw new Error("Use `discover`, `discover list`, `discover like <listing>`, or `discover pass <listing>`.");
9851
9998
  }
9852
9999
  async function runLikes(profileOverride) {
9853
10000
  await handleLikes(await ensureProfile(profileOverride));
@@ -9855,23 +10002,46 @@ function createMatchingCli(preset, options = {}) {
9855
10002
  async function runMatches(profileOverride) {
9856
10003
  await handleMatches(await ensureProfile(profileOverride));
9857
10004
  }
9858
- async function runChat(profileOverride, matchIdArg, flags = {}) {
9859
- await handleChat(await ensureProfile(profileOverride), matchIdArg, void 0, getStringFlag(flags, "message"));
10005
+ async function runChat(profileOverride, chatArg, args = [], flags = {}) {
10006
+ const profile = await ensureProfile(profileOverride);
10007
+ const message = getStringFlag(flags, "message");
10008
+ if (chatArg === "list") {
10009
+ await showConversationList(profile);
10010
+ return;
10011
+ }
10012
+ if (chatArg === "show") {
10013
+ await showConversationHistory(profile, args[0] ?? getStringFlag(flags, "thread-id"));
10014
+ return;
10015
+ }
10016
+ if (chatArg && message === void 0) {
10017
+ await showConversationHistory(profile, chatArg);
10018
+ return;
10019
+ }
10020
+ await handleChat(profile, chatArg, void 0, message);
9860
10021
  }
9861
10022
  async function runConfigCommand(subcommand, flags, profileOverride) {
9862
10023
  const profile = await ensureProfile(profileOverride);
9863
10024
  if (subcommand === "show") {
9864
- Vt(renderProfileCard(profile, true), "Advanced Config");
10025
+ showSection(renderProfileCard(profile, true), "Advanced Config");
9865
10026
  return;
9866
10027
  }
9867
10028
  if (subcommand === "relays") {
9868
10029
  const nextProfile = await promptRelayConfig(profile, getStringFlag(flags, "relays"));
9869
10030
  await store.saveProfile(nextProfile);
9870
- R.success("Relay list updated.");
10031
+ showSuccess("Relay list updated.");
9871
10032
  return;
9872
10033
  }
9873
10034
  throw new Error("Use `config show|relays`.");
9874
10035
  }
10036
+ async function runInbox(profileOverride) {
10037
+ await showInbox(await ensureProfile(profileOverride));
10038
+ }
10039
+ async function runWatch(profileOverride, flags) {
10040
+ const profile = await ensureProfile(profileOverride);
10041
+ const intervalSeconds = Number.parseInt(getStringFlag(flags, "interval") ?? "10", 10);
10042
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) throw new Error("Interval must be a positive number of seconds.");
10043
+ await watchInbox(profile, intervalSeconds);
10044
+ }
9875
10045
  async function ensureProfile(profileOverride) {
9876
10046
  const explicit = profileOverride ? await store.readProfile(profileOverride) : null;
9877
10047
  if (explicit) return explicit;
@@ -9890,19 +10060,58 @@ function createMatchingCli(preset, options = {}) {
9890
10060
  return promptProfileCreate();
9891
10061
  }
9892
10062
  async function promptProfileCreate(initial = {}) {
9893
- const profileName = await resolveRequiredInput(initial.profileName, {
9894
- message: "Choose a profile name.",
9895
- placeholder: "main",
9896
- defaultValue: "main",
10063
+ const draft = await collectProfileDraft(initial);
10064
+ const credentials = createGeneratedCredentials();
10065
+ return saveProfileAndOptionallyPublish(createDefaultProfile(preset, {
10066
+ profileName: draft.profileName,
10067
+ displayName: draft.displayName,
10068
+ bio: draft.bio,
10069
+ region: draft.region,
10070
+ ageRange: draft.ageRange,
10071
+ interests: splitComma(draft.interests),
10072
+ lookingFor: {
10073
+ ageRange: draft.lookingForAge,
10074
+ regions: splitComma(draft.lookingForRegions),
10075
+ notes: draft.lookingForNotes
10076
+ },
10077
+ nostr: credentials,
10078
+ relays: draft.relays
10079
+ }), "Preparing profile...", "Profile Created", true);
10080
+ }
10081
+ async function promptProfileImport(initial = {}) {
10082
+ const draft = await collectProfileDraft(initial);
10083
+ const credentials = importCredentials(await resolveRequiredInput(initial.nsec, {
10084
+ message: "Enter the nsec to import.",
10085
+ placeholder: "nsec1...",
9897
10086
  validate(value) {
9898
- const normalized = value?.trim() ?? "";
9899
- if (!normalized) return "Profile name is required.";
9900
- if (!/^[a-z0-9-]+$/.test(normalized)) return "Use lowercase letters, digits, and hyphens only.";
10087
+ try {
10088
+ importCredentials(value ?? "");
10089
+ } catch (error) {
10090
+ return error instanceof Error ? error.message : "Invalid nsec value.";
10091
+ }
9901
10092
  }
9902
- });
10093
+ }));
10094
+ return saveProfileAndOptionallyPublish(createDefaultProfile(preset, {
10095
+ profileName: draft.profileName,
10096
+ displayName: draft.displayName,
10097
+ bio: draft.bio,
10098
+ region: draft.region,
10099
+ ageRange: draft.ageRange,
10100
+ interests: splitComma(draft.interests),
10101
+ lookingFor: {
10102
+ ageRange: draft.lookingForAge,
10103
+ regions: splitComma(draft.lookingForRegions),
10104
+ notes: draft.lookingForNotes
10105
+ },
10106
+ nostr: credentials,
10107
+ relays: draft.relays
10108
+ }), "Importing profile...", initial.publish ? "Profile Imported & Published" : "Profile Imported", Boolean(initial.publish));
10109
+ }
10110
+ async function promptProfileEdit(profile, initial = {}) {
9903
10111
  const displayName = await resolveRequiredInput(initial.displayName, {
9904
10112
  message: "What display name should we show?",
9905
10113
  placeholder: preset.brand === "create-kanojo" ? "たくみ" : "あや",
10114
+ defaultValue: profile.displayName,
9906
10115
  validate(value) {
9907
10116
  if (!(value?.trim() ?? "")) return "Display name is required.";
9908
10117
  }
@@ -9910,6 +10119,7 @@ function createMatchingCli(preset, options = {}) {
9910
10119
  const ageRange = await resolveRequiredInput(initial.ageRange, {
9911
10120
  message: "How would you describe your age range?",
9912
10121
  placeholder: "20代後半",
10122
+ defaultValue: profile.ageRange,
9913
10123
  validate(value) {
9914
10124
  if (!(value?.trim() ?? "")) return "Age range is required.";
9915
10125
  }
@@ -9917,6 +10127,7 @@ function createMatchingCli(preset, options = {}) {
9917
10127
  const region = await resolveRequiredInput(initial.region, {
9918
10128
  message: "Which area do you usually meet in?",
9919
10129
  placeholder: "東京",
10130
+ defaultValue: profile.region,
9920
10131
  validate(value) {
9921
10132
  if (!(value?.trim() ?? "")) return "Region is required.";
9922
10133
  }
@@ -9924,51 +10135,117 @@ function createMatchingCli(preset, options = {}) {
9924
10135
  const bio = await resolveRequiredInput(initial.bio, {
9925
10136
  message: "Write a short intro.",
9926
10137
  placeholder: "映画とコーヒーが好きです。",
10138
+ defaultValue: profile.bio,
9927
10139
  validate(value) {
9928
10140
  if (!(value?.trim() ?? "")) return "Bio is required.";
9929
10141
  }
9930
10142
  });
9931
10143
  const interests = await resolveOptionalInput(initial.interests, {
9932
10144
  message: "List interests or vibe tags, comma separated.",
10145
+ defaultValue: profile.interests.join(", "),
9933
10146
  placeholder: "映画, カフェ, 散歩"
9934
10147
  });
9935
10148
  const lookingForAge = await resolveOptionalInput(initial.lookingForAge, {
9936
10149
  message: "What age range are you looking for?",
10150
+ defaultValue: profile.lookingFor.ageRange,
9937
10151
  placeholder: "20代"
9938
10152
  });
9939
10153
  const lookingForRegions = await resolveOptionalInput(initial.lookingForRegions, {
9940
10154
  message: "Which regions do you want to meet in? Use commas.",
10155
+ defaultValue: profile.lookingFor.regions.join(", "),
9941
10156
  placeholder: region
9942
10157
  });
9943
10158
  const lookingForNotes = await resolveOptionalInput(initial.lookingForNotes, {
9944
10159
  message: "What kind of person feels right for you?",
10160
+ defaultValue: profile.lookingFor.notes,
9945
10161
  placeholder: "落ち着いて話せる人"
9946
10162
  });
9947
- const relays = initial.relays ? normalizeRelayList(initial.relays) : DEFAULT_RELAYS;
9948
- const credentials = createGeneratedCredentials();
9949
- const profile = createDefaultProfile(preset, {
9950
- profileName,
10163
+ return saveProfileAndOptionallyPublish({
10164
+ ...profile,
9951
10165
  displayName,
9952
- bio,
9953
- region,
9954
10166
  ageRange,
10167
+ region,
10168
+ bio,
9955
10169
  interests: splitComma(interests),
9956
10170
  lookingFor: {
9957
10171
  ageRange: lookingForAge,
9958
10172
  regions: splitComma(lookingForRegions),
9959
10173
  notes: lookingForNotes
9960
- },
9961
- nostr: credentials,
9962
- relays
10174
+ }
10175
+ }, "Updating profile...", "Profile Updated", true);
10176
+ }
10177
+ async function collectProfileDraft(initial) {
10178
+ const profileName = await resolveRequiredInput(initial.profileName, {
10179
+ message: "Choose a profile name.",
10180
+ placeholder: "main",
10181
+ defaultValue: "main",
10182
+ validate(value) {
10183
+ const normalized = value?.trim() ?? "";
10184
+ if (!normalized) return "Profile name is required.";
10185
+ if (!/^[a-z0-9-]+$/.test(normalized)) return "Use lowercase letters, digits, and hyphens only.";
10186
+ }
10187
+ });
10188
+ const displayName = await resolveRequiredInput(initial.displayName, {
10189
+ message: "What display name should we show?",
10190
+ placeholder: preset.brand === "create-kanojo" ? "たくみ" : "あや",
10191
+ validate(value) {
10192
+ if (!(value?.trim() ?? "")) return "Display name is required.";
10193
+ }
10194
+ });
10195
+ const ageRange = await resolveRequiredInput(initial.ageRange, {
10196
+ message: "How would you describe your age range?",
10197
+ placeholder: "20代後半",
10198
+ validate(value) {
10199
+ if (!(value?.trim() ?? "")) return "Age range is required.";
10200
+ }
9963
10201
  });
9964
- const published = await withSpinner("Preparing profile...", async () => {
10202
+ const region = await resolveRequiredInput(initial.region, {
10203
+ message: "Which area do you usually meet in?",
10204
+ placeholder: "東京",
10205
+ validate(value) {
10206
+ if (!(value?.trim() ?? "")) return "Region is required.";
10207
+ }
10208
+ });
10209
+ return {
10210
+ profileName,
10211
+ displayName,
10212
+ ageRange,
10213
+ region,
10214
+ bio: await resolveRequiredInput(initial.bio, {
10215
+ message: "Write a short intro.",
10216
+ placeholder: "映画とコーヒーが好きです。",
10217
+ validate(value) {
10218
+ if (!(value?.trim() ?? "")) return "Bio is required.";
10219
+ }
10220
+ }),
10221
+ interests: await resolveOptionalInput(initial.interests, {
10222
+ message: "List interests or vibe tags, comma separated.",
10223
+ placeholder: "映画, カフェ, 散歩"
10224
+ }),
10225
+ lookingForAge: await resolveOptionalInput(initial.lookingForAge, {
10226
+ message: "What age range are you looking for?",
10227
+ placeholder: "20代"
10228
+ }),
10229
+ lookingForRegions: await resolveOptionalInput(initial.lookingForRegions, {
10230
+ message: "Which regions do you want to meet in? Use commas.",
10231
+ placeholder: region
10232
+ }),
10233
+ lookingForNotes: await resolveOptionalInput(initial.lookingForNotes, {
10234
+ message: "What kind of person feels right for you?",
10235
+ placeholder: "落ち着いて話せる人"
10236
+ }),
10237
+ relays: initial.relays ? normalizeRelayList(initial.relays) : DEFAULT_RELAYS
10238
+ };
10239
+ }
10240
+ async function saveProfileAndOptionallyPublish(profile, spinnerMessage, title, publish) {
10241
+ const nextProfile = publish ? await runWithSpinner(spinnerMessage, async () => {
9965
10242
  await service.publishProfile(profile);
9966
10243
  return profile;
9967
- });
9968
- await store.saveProfile(published);
9969
- await store.setActiveProfile(preset.brand, published.profileName);
9970
- Vt(renderProfileCard(published), "Profile Created");
9971
- return published;
10244
+ }) : profile;
10245
+ await store.saveProfile(nextProfile);
10246
+ await store.setActiveProfile(preset.brand, nextProfile.profileName);
10247
+ showSection(renderProfileCard(nextProfile), title);
10248
+ return nextProfile;
9972
10249
  }
9973
10250
  async function promptProfileUse(profileNameArg) {
9974
10251
  const profiles = await store.listProfiles();
@@ -9988,7 +10265,7 @@ function createMatchingCli(preset, options = {}) {
9988
10265
  const profile = await store.readProfile(selectedName);
9989
10266
  if (!profile) throw new Error(`Profile not found: ${selectedName}`);
9990
10267
  await store.setActiveProfile(preset.brand, selectedName);
9991
- Vt(renderProfileCard(profile), "Active Profile");
10268
+ showSection(renderProfileCard(profile), "Active Profile");
9992
10269
  return profile;
9993
10270
  }
9994
10271
  async function handlePublishListing(profile, initial = {}) {
@@ -10014,44 +10291,90 @@ function createMatchingCli(preset, options = {}) {
10014
10291
  message: "Enter tags, comma separated.",
10015
10292
  placeholder: "映画, 落ち着き, 夜カフェ"
10016
10293
  });
10017
- const nextProfile = await withSpinner("Publishing listing...", () => service.publishListing(profile, {
10294
+ const nextProfile = await runWithSpinner("Publishing listing...", () => service.publishListing(profile, {
10018
10295
  headline,
10019
10296
  summary,
10020
10297
  region,
10021
10298
  desiredTags: splitComma(desiredTags)
10022
10299
  }));
10023
10300
  await store.saveProfile(nextProfile);
10024
- R.success("Listing published.");
10301
+ showSuccess("Listing published.");
10025
10302
  return nextProfile;
10026
10303
  }
10027
- async function handleDiscover(profile) {
10028
- const refreshed = await withSpinner("Looking for people...", async () => {
10029
- const synced = await service.syncInbox(profile);
10030
- const withListings = await service.refreshOwnListings(synced);
10031
- return {
10032
- profile: withListings,
10033
- listings: rankDiscoverListings(withListings, await service.discoverListings(withListings))
10034
- };
10304
+ async function promptListingEdit(profile, initial = {}) {
10305
+ const listing = await resolveOwnListing(profile, initial.listingRef, false);
10306
+ const headline = await resolveRequiredInput(initial.headline, {
10307
+ message: "Enter the listing title.",
10308
+ placeholder: "週末に一緒に映画を見に行ける人",
10309
+ defaultValue: listing.headline,
10310
+ validate(value) {
10311
+ if (!(value?.trim() ?? "")) return "Title is required.";
10312
+ }
10035
10313
  });
10036
- await store.saveProfile(refreshed.profile);
10314
+ const summary = await resolveRequiredInput(initial.summary, {
10315
+ message: "Write a short summary.",
10316
+ placeholder: "まずはお茶からゆっくり話したいです。",
10317
+ defaultValue: listing.summary,
10318
+ validate(value) {
10319
+ if (!(value?.trim() ?? "")) return "Summary is required.";
10320
+ }
10321
+ });
10322
+ const region = await resolveOptionalInput(initial.region, {
10323
+ message: "Which region is this listing for?",
10324
+ defaultValue: listing.region
10325
+ });
10326
+ const desiredTags = await resolveOptionalInput(initial.desiredTags, {
10327
+ message: "Enter tags, comma separated.",
10328
+ defaultValue: listing.desiredTags.join(", "),
10329
+ placeholder: "映画, 落ち着き, 夜カフェ"
10330
+ });
10331
+ const nextProfile = await runWithSpinner("Updating listing...", () => service.updateListing(profile, {
10332
+ listingId: listing.id,
10333
+ headline,
10334
+ summary,
10335
+ region,
10336
+ desiredTags: splitComma(desiredTags)
10337
+ }));
10338
+ await store.saveProfile(nextProfile);
10339
+ showSuccess("Listing updated.");
10340
+ return nextProfile;
10341
+ }
10342
+ async function reopenListing(profile, listingRef) {
10343
+ const listing = await resolveOwnListing(profile, listingRef, true);
10344
+ const nextProfile = await runWithSpinner("Reopening listing...", () => service.updateListing(profile, {
10345
+ listingId: listing.id,
10346
+ status: "open"
10347
+ }));
10348
+ await store.saveProfile(nextProfile);
10349
+ showSuccess("Listing reopened.");
10350
+ return nextProfile;
10351
+ }
10352
+ async function resolveOwnListing(profile, listingRef, closedOnly = false) {
10353
+ const listings = profile.cache.listings.filter((listing) => closedOnly ? listing.status === "closed" : true);
10354
+ if (listings.length === 0) throw new Error(closedOnly ? "There are no closed listings to reopen." : "There are no listings yet.");
10355
+ const selected = (listingRef ? listings.find((listing) => listing.id === listingRef || listing.address === listingRef) : null) ?? null;
10356
+ if (listingRef && !selected) throw new Error("Listing not found. Use `listing list` to check the id or address.");
10357
+ if (selected) return selected;
10358
+ const listingId = await askSelect({
10359
+ message: closedOnly ? "Choose a listing to reopen." : "Choose a listing to edit.",
10360
+ options: listings.map((listing) => ({
10361
+ value: listing.id,
10362
+ label: listing.headline,
10363
+ hint: `${listing.region} | ${listing.status}`
10364
+ }))
10365
+ });
10366
+ const listing = listings.find((item) => item.id === listingId);
10367
+ if (!listing) throw new Error("Listing not found.");
10368
+ return listing;
10369
+ }
10370
+ async function handleDiscover(profile) {
10371
+ const refreshed = await loadDiscoverState(profile);
10037
10372
  if (refreshed.listings.length === 0) {
10038
- R.info("No listings found right now. Try again later.");
10373
+ showInfo("No listings found right now. Try again later.");
10039
10374
  return refreshed.profile;
10040
10375
  }
10041
- const openListings = refreshed.profile.cache.listings.filter((item) => item.status === "open");
10042
- if (openListings.length === 0) {
10043
- R.warn("Publish at least one open listing first.");
10044
- return refreshed.profile;
10045
- }
10046
- const ownListingAddress = openListings.length === 1 ? openListings[0].address : await askSelect({
10047
- message: "Which of your listings should send the like?",
10048
- options: openListings.map((item) => ({
10049
- value: item.address,
10050
- label: item.headline,
10051
- hint: item.region
10052
- }))
10053
- });
10054
- Vt([
10376
+ const ownListingAddress = await resolveOwnOpenListingAddress(refreshed.profile);
10377
+ showSection([
10055
10378
  "y: send like",
10056
10379
  "n: pass for now",
10057
10380
  "q: quit discover",
@@ -10065,38 +10388,144 @@ function createMatchingCli(preset, options = {}) {
10065
10388
  while (queue.length > 0) {
10066
10389
  const current = queue[0];
10067
10390
  if (!current) break;
10068
- Vt(renderDiscoverCard(current, queue.length), "Next Candidate");
10391
+ showSection(renderDiscoverCard(current, queue.length), "Next Candidate");
10069
10392
  const action = await askSwipeAction();
10070
10393
  if (action === "quit") break;
10071
- nextProfile = recordSwipeDecision(nextProfile, current, action);
10072
10394
  if (action === "yes") {
10073
- nextProfile = await withSpinner("Sending like...", () => service.sendLike(nextProfile, {
10074
- fromListing: ownListingAddress,
10075
- toListing: current.address,
10076
- fromProfileName: nextProfile.profileName,
10077
- recipientPubkey: current.authorPubkey,
10078
- recipientRelays: current.inboxRelays
10079
- }));
10395
+ nextProfile = await sendLikeToDiscoveredListing(nextProfile, current, ownListingAddress);
10080
10396
  likedCount += 1;
10081
- R.success(`Sent a like to ${current.profileDisplayName}.`);
10397
+ showSuccess(`Sent a like to ${current.profileDisplayName}.`);
10082
10398
  } else {
10399
+ nextProfile = recordSwipeDecision(nextProfile, current, action);
10083
10400
  skippedCount += 1;
10084
- R.step(`Passed on ${current.profileDisplayName} for now.`);
10401
+ showStep(`Passed on ${current.profileDisplayName} for now.`);
10085
10402
  }
10086
10403
  await store.saveProfile(nextProfile);
10087
10404
  queue = rankDiscoverListings(nextProfile, refreshed.listings);
10088
10405
  }
10089
- Vt([
10406
+ showSection([
10090
10407
  `Likes: ${likedCount}`,
10091
10408
  `Passes: ${skippedCount}`,
10092
10409
  `Remaining: ${queue.length}`
10093
10410
  ].join("\n"), "Discover Summary");
10094
10411
  return nextProfile;
10095
10412
  }
10413
+ async function showDiscoverList(profile) {
10414
+ const refreshed = await loadDiscoverState(profile);
10415
+ if (refreshed.listings.length === 0) {
10416
+ showInfo("No listings found right now. Try again later.");
10417
+ return;
10418
+ }
10419
+ showSection(renderDiscoverListings(refreshed.listings), "Discover");
10420
+ }
10421
+ async function likeDiscoveredListing(profile, listingRef, flags) {
10422
+ if (!listingRef) throw new Error("Use `discover like <listing-id|address>`.");
10423
+ const refreshed = await loadDiscoverState(profile);
10424
+ const listing = resolveDiscoveredListing(refreshed.listings, listingRef);
10425
+ const ownListingAddress = await resolveOwnOpenListingAddress(refreshed.profile, getStringFlag(flags, "from", "from-listing"));
10426
+ const nextProfile = await sendLikeToDiscoveredListing(refreshed.profile, listing, ownListingAddress);
10427
+ await store.saveProfile(nextProfile);
10428
+ showSection(renderDiscoverCard(listing, refreshed.listings.length), "Liked");
10429
+ showSuccess(`Sent a like to ${listing.profileDisplayName}.`);
10430
+ }
10431
+ async function passDiscoveredListing(profile, listingRef) {
10432
+ if (!listingRef) throw new Error("Use `discover pass <listing-id|address>`.");
10433
+ const refreshed = await loadDiscoverState(profile);
10434
+ const listing = resolveDiscoveredListing(refreshed.listings, listingRef);
10435
+ const nextProfile = recordSwipeDecision(refreshed.profile, listing, "no");
10436
+ await store.saveProfile(nextProfile);
10437
+ showSection(renderDiscoverCard(listing, refreshed.listings.length), "Passed");
10438
+ showSuccess(`Passed on ${listing.profileDisplayName}.`);
10439
+ }
10440
+ async function loadDiscoverState(profile) {
10441
+ const refreshed = await runWithSpinner("Looking for people...", async () => {
10442
+ const synced = await service.syncInbox(profile);
10443
+ const withListings = await service.refreshOwnListings(synced);
10444
+ return {
10445
+ profile: withListings,
10446
+ listings: rankDiscoverListings(withListings, await service.discoverListings(withListings))
10447
+ };
10448
+ });
10449
+ await store.saveProfile(refreshed.profile);
10450
+ return refreshed;
10451
+ }
10452
+ async function resolveOwnOpenListingAddress(profile, listingRef) {
10453
+ const openListings = profile.cache.listings.filter((item) => item.status === "open");
10454
+ if (openListings.length === 0) throw new Error("Publish at least one open listing first.");
10455
+ const selected = (listingRef ? openListings.find((item) => item.id === listingRef || item.address === listingRef) : null) ?? null;
10456
+ if (listingRef && !selected) throw new Error("Own listing not found. Use `listing list` to check the id or address.");
10457
+ if (selected) return selected.address;
10458
+ if (openListings.length === 1) return openListings[0].address;
10459
+ return await askSelect({
10460
+ message: "Which of your listings should send the like?",
10461
+ options: openListings.map((item) => ({
10462
+ value: item.address,
10463
+ label: item.headline,
10464
+ hint: item.region
10465
+ }))
10466
+ });
10467
+ }
10468
+ function resolveDiscoveredListing(listings, listingRef) {
10469
+ const listing = listings.find((item) => item.id === listingRef || item.address === listingRef);
10470
+ if (!listing) throw new Error("Discover target not found. Use `discover list` to inspect available listings.");
10471
+ return listing;
10472
+ }
10473
+ async function sendLikeToDiscoveredListing(profile, listing, ownListingAddress) {
10474
+ const withDecision = recordSwipeDecision(profile, listing, "yes");
10475
+ return runWithSpinner("Sending like...", () => service.sendLike(withDecision, {
10476
+ fromListing: ownListingAddress,
10477
+ toListing: listing.address,
10478
+ fromProfileName: withDecision.profileName,
10479
+ recipientPubkey: listing.authorPubkey,
10480
+ recipientRelays: listing.inboxRelays
10481
+ }));
10482
+ }
10483
+ async function showInbox(profile) {
10484
+ const nextProfile = await syncInboxState(profile);
10485
+ showSection(renderInboxSummary(nextProfile), "Inbox");
10486
+ const conversations = buildConversations(nextProfile);
10487
+ if (conversations.length > 0) showSection(renderConversationList(conversations.slice(0, 5)), "Recent Conversations");
10488
+ return nextProfile;
10489
+ }
10490
+ async function watchInbox(profile, intervalSeconds) {
10491
+ let current = await showInbox(profile);
10492
+ showInfo(`Watching inbox every ${intervalSeconds}s. Press Ctrl+C to stop.`);
10493
+ let stopped = false;
10494
+ const onSigint = () => {
10495
+ stopped = true;
10496
+ };
10497
+ process.once("SIGINT", onSigint);
10498
+ try {
10499
+ while (!stopped) {
10500
+ await sleep(intervalSeconds * 1e3);
10501
+ if (stopped) break;
10502
+ const nextProfile = await syncInboxState(current);
10503
+ if (!hasInboxChanged(current, nextProfile)) continue;
10504
+ current = nextProfile;
10505
+ showSection(renderInboxSummary(current), "Inbox Update");
10506
+ const conversations = buildConversations(current);
10507
+ if (conversations.length > 0) showSection(renderConversationList(conversations.slice(0, 3)), "Recent Conversations");
10508
+ }
10509
+ } finally {
10510
+ process.off("SIGINT", onSigint);
10511
+ showInfo("Stopped watching inbox.");
10512
+ }
10513
+ }
10514
+ async function syncInboxState(profile) {
10515
+ const nextProfile = await runWithSpinner("Syncing inbox...", () => service.syncInbox(profile));
10516
+ await store.saveProfile(nextProfile);
10517
+ return nextProfile;
10518
+ }
10519
+ function hasInboxChanged(previous, next) {
10520
+ if (previous.cache.likesReceived.length !== next.cache.likesReceived.length) return true;
10521
+ if (previous.cache.matches.length !== next.cache.matches.length) return true;
10522
+ if (previous.cache.chatMessages.length !== next.cache.chatMessages.length) return true;
10523
+ return previous.cache.lastInboxSyncAt !== next.cache.lastInboxSyncAt;
10524
+ }
10096
10525
  async function handleLikes(profile) {
10097
- const nextProfile = await withSpinner("Syncing likes...", () => service.syncInbox(profile));
10526
+ const nextProfile = await runWithSpinner("Syncing likes...", () => service.syncInbox(profile));
10098
10527
  await store.saveProfile(nextProfile);
10099
- Vt(renderLikes(nextProfile), "Likes");
10528
+ showSection(renderLikes(nextProfile), "Likes");
10100
10529
  const conversations = getLikedYouConversations(nextProfile);
10101
10530
  if (conversations.length === 0) return nextProfile;
10102
10531
  if (!await askConfirm({
@@ -10106,13 +10535,13 @@ function createMatchingCli(preset, options = {}) {
10106
10535
  return handleChat(nextProfile, void 0, conversations);
10107
10536
  }
10108
10537
  async function handleMatches(profile) {
10109
- const nextProfile = await withSpinner("Syncing matches...", () => service.syncInbox(profile));
10538
+ const nextProfile = await runWithSpinner("Syncing matches...", () => service.syncInbox(profile));
10110
10539
  await store.saveProfile(nextProfile);
10111
10540
  if (nextProfile.cache.matches.length === 0) {
10112
- R.info("No matches yet. Mutual likes will appear here.");
10541
+ showInfo("No matches yet. Mutual likes will appear here.");
10113
10542
  return nextProfile;
10114
10543
  }
10115
- Vt(renderMatches(nextProfile.cache.matches), "Matches");
10544
+ showSection(renderMatches(nextProfile.cache.matches), "Matches");
10116
10545
  if (await askConfirm({
10117
10546
  message: "Open one now?",
10118
10547
  initialValue: true
@@ -10122,12 +10551,12 @@ function createMatchingCli(preset, options = {}) {
10122
10551
  async function handleChat(profile, threadIdArg, availableConversations, initialMessage) {
10123
10552
  let nextProfile = profile;
10124
10553
  if (!availableConversations) {
10125
- nextProfile = await withSpinner("Syncing conversation...", () => service.syncInbox(profile));
10554
+ nextProfile = await runWithSpinner("Syncing conversation...", () => service.syncInbox(profile));
10126
10555
  await store.saveProfile(nextProfile);
10127
10556
  }
10128
10557
  const conversations = availableConversations ?? buildConversations(nextProfile);
10129
10558
  if (conversations.length === 0) {
10130
- R.info("There are no conversations yet.");
10559
+ showInfo("There are no conversations yet.");
10131
10560
  return nextProfile;
10132
10561
  }
10133
10562
  const conversation = (threadIdArg ? conversations.find((item) => item.threadId === threadIdArg) : null) ?? await promptConversation(conversations);
@@ -10137,7 +10566,7 @@ function createMatchingCli(preset, options = {}) {
10137
10566
  if (!body) throw new Error("Message is required.");
10138
10567
  return sendChatMessage(nextProfile, conversation, body);
10139
10568
  }
10140
- Vt(renderConversation(conversation), `chat | ${conversation.peerProfileName}`);
10569
+ showSection(renderConversation(conversation), `chat | ${conversation.peerProfileName}`);
10141
10570
  while (true) {
10142
10571
  const body = await askText({
10143
10572
  message: "Enter a message. Leave it blank to exit.",
@@ -10152,8 +10581,27 @@ function createMatchingCli(preset, options = {}) {
10152
10581
  })) return nextProfile;
10153
10582
  }
10154
10583
  }
10584
+ async function showConversationList(profile) {
10585
+ const conversations = buildConversations(await syncConversations(profile));
10586
+ if (conversations.length === 0) {
10587
+ showInfo("There are no conversations yet.");
10588
+ return;
10589
+ }
10590
+ showSection(renderConversationList(conversations), "Conversations");
10591
+ }
10592
+ async function showConversationHistory(profile, threadIdArg) {
10593
+ if (!threadIdArg) throw new Error("Use `chat show <thread-id>` or `chat <thread-id>`.");
10594
+ const conversation = buildConversations(await syncConversations(profile)).find((item) => item.threadId === threadIdArg);
10595
+ if (!conversation) throw new Error("Conversation not found.");
10596
+ showSection(renderConversation(conversation), `chat | ${conversation.peerProfileName}`);
10597
+ }
10598
+ async function syncConversations(profile) {
10599
+ const nextProfile = await runWithSpinner("Syncing conversation...", () => service.syncInbox(profile));
10600
+ await store.saveProfile(nextProfile);
10601
+ return nextProfile;
10602
+ }
10155
10603
  async function sendChatMessage(profile, conversation, body) {
10156
- const nextProfile = await withSpinner("Sending message...", () => service.sendChat(profile, {
10604
+ const nextProfile = await runWithSpinner("Sending message...", () => service.sendChat(profile, {
10157
10605
  matchId: conversation.threadId,
10158
10606
  recipientPubkey: conversation.peerPubkey,
10159
10607
  recipientRelays: conversation.peerRelays,
@@ -10161,8 +10609,8 @@ function createMatchingCli(preset, options = {}) {
10161
10609
  }));
10162
10610
  await store.saveProfile(nextProfile);
10163
10611
  const refreshedConversation = buildConversations(nextProfile).find((item) => item.threadId === conversation.threadId);
10164
- if (refreshedConversation) Vt(renderConversation(refreshedConversation), `chat | ${refreshedConversation.peerProfileName}`);
10165
- R.success("Message sent.");
10612
+ if (refreshedConversation) showSection(renderConversation(refreshedConversation), `chat | ${refreshedConversation.peerProfileName}`);
10613
+ showSuccess("Message sent.");
10166
10614
  return nextProfile;
10167
10615
  }
10168
10616
  async function promptConfig(profile) {
@@ -10186,14 +10634,14 @@ function createMatchingCli(preset, options = {}) {
10186
10634
  ]
10187
10635
  });
10188
10636
  if (action === "show") {
10189
- Vt(renderProfileCard(profile, true), "Advanced Config");
10637
+ showSection(renderProfileCard(profile, true), "Advanced Config");
10190
10638
  return profile;
10191
10639
  }
10192
10640
  if (action === "relays") return promptRelayConfig(profile);
10193
10641
  return profile;
10194
10642
  }
10195
10643
  async function promptRelayConfig(profile, relayInputArg) {
10196
- Vt(profile.relays.join("\n"), "Current Relays");
10644
+ showSection(profile.relays.join("\n"), "Current Relays");
10197
10645
  const relayInput = relayInputArg ?? await askText({
10198
10646
  message: "Enter new relays, comma separated.",
10199
10647
  defaultValue: profile.relays.join(", "),
@@ -10205,7 +10653,7 @@ function createMatchingCli(preset, options = {}) {
10205
10653
  }
10206
10654
  }
10207
10655
  });
10208
- const nextProfile = await withSpinner("Updating relays...", () => service.updateRelays(profile, normalizeRelayList(relayInput)));
10656
+ const nextProfile = await runWithSpinner("Updating relays...", () => service.updateRelays(profile, normalizeRelayList(relayInput)));
10209
10657
  await store.saveProfile(nextProfile);
10210
10658
  return nextProfile;
10211
10659
  }
@@ -10230,9 +10678,21 @@ function renderCliUsage(preset) {
10230
10678
  " profile list",
10231
10679
  " profile use <name>",
10232
10680
  " profile show",
10681
+ " profile edit --display-name \"<name>\" --bio \"...\"",
10682
+ " profile import --name main --nsec nsec1... --publish",
10233
10683
  " profile create --name main --display-name \"<name>\" --age-range \"20代後半\" --region 東京 --bio \"映画とコーヒーが好きです。\" --interests \"映画, カフェ\" --looking-age \"20代\" --looking-regions \"東京, 神奈川\" --looking-notes \"落ち着いて話せる人\"",
10234
10684
  " listing publish --title \"週末に一緒に映画を見に行ける人\" --summary \"まずはお茶からゆっくり話したいです。\" --region 東京 --tags \"映画, 夜カフェ\"",
10685
+ " listing edit <listing-id> --title \"更新タイトル\"",
10235
10686
  " listing close --id <listing-id>",
10687
+ " listing reopen <listing-id>",
10688
+ " discover list",
10689
+ " discover like <listing-id> --from <your-listing-id>",
10690
+ " discover pass <listing-id>",
10691
+ " inbox",
10692
+ " watch --interval 10",
10693
+ " chat list",
10694
+ " chat show <thread-id>",
10695
+ " chat <thread-id>",
10236
10696
  " chat <thread-id> --message \"こんにちは\"",
10237
10697
  " config relays --relays \"wss://relay1.example,wss://relay2.example\"",
10238
10698
  "",
@@ -10317,7 +10777,8 @@ async function askConfirm(options) {
10317
10777
  if (Ct$1(answer)) throw new CancelledFlowError();
10318
10778
  return Boolean(answer);
10319
10779
  }
10320
- async function withSpinner(message, task) {
10780
+ async function withSpinner(message, task, plain = false) {
10781
+ if (plain) return task();
10321
10782
  const indicator = be();
10322
10783
  indicator.start(message);
10323
10784
  try {
@@ -10366,10 +10827,12 @@ function renderProfileCard(profile, advanced = false) {
10366
10827
  }
10367
10828
  function renderListings(listings) {
10368
10829
  if (listings.length === 0) return "No listings yet.";
10369
- return listings.map((listing) => `${formatListingChoiceLabel(listing)}\n ${listing.summary}\n ${listing.address}`).join("\n\n");
10830
+ return listings.map((listing) => `${formatListingChoiceLabel(listing)}\n id: ${listing.id}\n updated: ${formatTimestamp(listing.updatedAt)}\n ${listing.summary}\n ${listing.address}`).join("\n\n");
10370
10831
  }
10371
10832
  function renderDiscoverCard(listing, remainingCount) {
10372
10833
  return [
10834
+ `id: ${listing.id}`,
10835
+ `address: ${listing.address}`,
10373
10836
  `score: ${listing.score}`,
10374
10837
  `Remaining: ${remainingCount}`,
10375
10838
  `${listing.profileDisplayName} | ${listing.region}`,
@@ -10381,13 +10844,16 @@ function renderDiscoverCard(listing, remainingCount) {
10381
10844
  `Why: ${listing.reasons.join(" / ") || "Exploring new patterns"}`
10382
10845
  ].join("\n");
10383
10846
  }
10847
+ function renderDiscoverListings(listings) {
10848
+ return listings.map((listing, index) => `${index + 1}. ${listing.profileDisplayName} | ${listing.headline}\n id: ${listing.id}\n address: ${listing.address}\n region: ${listing.region}\n score: ${listing.score}\n tags: ${listing.desiredTags.join(", ") || "None"}\n why: ${listing.reasons.join(" / ") || "Exploring new patterns"}`).join("\n\n");
10849
+ }
10384
10850
  function renderLikes(profile) {
10385
- const sent = profile.cache.likesSent.map((like) => `→ ${like.toListing}\n ${like.fromProfileName} / ${(/* @__PURE__ */ new Date(like.createdAt * 1e3)).toLocaleString("en-US")}`).join("\n\n");
10851
+ const sent = profile.cache.likesSent.map((like) => `→ ${like.toListing}\n ${like.fromProfileName} / ${formatTimestamp(like.createdAt)}`).join("\n\n");
10386
10852
  const likedYouMap = new Map(getLikedYouConversations(profile).map((conversation) => [conversation.threadId, conversation]));
10387
10853
  const received = profile.cache.likesReceived.map((like) => {
10388
10854
  const conversation = likedYouMap.get(like.matchId);
10389
10855
  const dmState = conversation ? `DM ready (${conversation.messages.length} msgs)` : "DM ready";
10390
- return `← ${like.fromProfileName}\n ${like.fromListing}\n ${(/* @__PURE__ */ new Date(like.createdAt * 1e3)).toLocaleString("en-US")}\n ${dmState}`;
10856
+ return `← ${like.fromProfileName}\n ${like.fromListing}\n ${formatTimestamp(like.createdAt)}\n ${dmState}`;
10391
10857
  }).join("\n\n");
10392
10858
  return [
10393
10859
  "Sent Likes",
@@ -10398,20 +10864,39 @@ function renderLikes(profile) {
10398
10864
  ].join("\n");
10399
10865
  }
10400
10866
  function renderMatches(matches) {
10401
- return matches.map((match) => `${match.peerProfileName}\n ${match.matchId}\n Updated: ${(/* @__PURE__ */ new Date(match.updatedAt * 1e3)).toLocaleString("en-US")}`).join("\n\n");
10867
+ return matches.map((match) => `${match.peerProfileName}\n ${match.matchId}\n Updated: ${formatTimestamp(match.updatedAt)}`).join("\n\n");
10402
10868
  }
10403
10869
  function renderConversation(conversation) {
10404
10870
  const messages = conversation.messages.map((message) => {
10405
- return `${message.senderPubkey === conversation.peerPubkey ? conversation.peerProfileName : "You"}: ${message.body}`;
10871
+ const speaker = message.senderPubkey === conversation.peerPubkey ? conversation.peerProfileName : "You";
10872
+ return `[${formatTimestamp(message.createdAt)}] ${speaker} (${message.rumorId}): ${message.body}`;
10406
10873
  }).join("\n");
10407
10874
  return [
10408
10875
  `Conversation with ${conversation.peerProfileName}`,
10409
10876
  `Thread: ${conversation.threadId}`,
10410
10877
  `Source: ${conversation.source}`,
10878
+ `Messages: ${conversation.messages.length}`,
10879
+ `Updated: ${formatTimestamp(conversation.updatedAt)}`,
10411
10880
  "",
10412
10881
  messages || "No messages yet."
10413
10882
  ].join("\n");
10414
10883
  }
10884
+ function renderConversationList(conversations) {
10885
+ return conversations.map((conversation) => `${conversation.peerProfileName}\n ${conversation.threadId}\n Source: ${conversation.source}\n Messages: ${conversation.messages.length}\n Updated: ${formatTimestamp(conversation.updatedAt)}`).join("\n\n");
10886
+ }
10887
+ function renderInboxSummary(profile) {
10888
+ const latestConversation = buildConversations(profile)[0];
10889
+ return [
10890
+ `Last Sync: ${profile.cache.lastInboxSyncAt ? formatTimestamp(profile.cache.lastInboxSyncAt) : "Never"}`,
10891
+ `Likes Received: ${profile.cache.likesReceived.length}`,
10892
+ `Matches: ${profile.cache.matches.length}`,
10893
+ `Messages: ${profile.cache.chatMessages.length}`,
10894
+ `Latest Thread: ${latestConversation ? `${latestConversation.peerProfileName} (${formatTimestamp(latestConversation.updatedAt)})` : "None"}`
10895
+ ].join("\n");
10896
+ }
10897
+ function formatTimestamp(unixSeconds) {
10898
+ return (/* @__PURE__ */ new Date(unixSeconds * 1e3)).toLocaleString("ja-JP");
10899
+ }
10415
10900
  function splitComma(value) {
10416
10901
  return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
10417
10902
  }
@@ -10447,6 +10932,9 @@ function normalizeRelayList(value) {
10447
10932
  if (!items.every((item) => item.startsWith("wss://"))) throw new Error("Relay URLs must start with wss://.");
10448
10933
  return items;
10449
10934
  }
10935
+ function sleep(ms) {
10936
+ return new Promise((resolve) => setTimeout(resolve, ms));
10937
+ }
10450
10938
  async function askSwipeAction() {
10451
10939
  if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
10452
10940
  const normalized = normalizeSwipeAction(await askText({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kanojo",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A Nostr-based dating CLI for finding a kanojo.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "vite-plus": "^0.1.11"
28
28
  },
29
29
  "peerDependencies": {
30
- "typescript": "^5",
31
- "@repo/shared": "@repo/shared"
30
+ "@repo/shared": "workspace:*",
31
+ "typescript": "^5"
32
32
  }
33
33
  }