fluncle 0.65.0 → 0.66.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.
Files changed (2) hide show
  1. package/bin/fluncle.mjs +287 -34
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -423,7 +423,7 @@ function parseVersion(version) {
423
423
  var currentVersion;
424
424
  var init_version = __esm(() => {
425
425
  init_output();
426
- currentVersion = "0.65.0".trim() ? "0.65.0".trim() : "0.1.0";
426
+ currentVersion = "0.66.0".trim() ? "0.66.0".trim() : "0.1.0";
427
427
  });
428
428
 
429
429
  // src/update-notifier.ts
@@ -657,7 +657,68 @@ var init_env = __esm(() => {
657
657
  envProfiles2 = ["local", "production"];
658
658
  });
659
659
 
660
+ // src/user-token.ts
661
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
662
+ import { homedir as homedir3 } from "node:os";
663
+ import { dirname as dirname2, join as join3 } from "node:path";
664
+ function activeProfile() {
665
+ const profile = process.env.FLUNCLE_ENV ?? defaultEnvProfile2;
666
+ return envProfiles3.includes(profile) ? profile : defaultEnvProfile2;
667
+ }
668
+ function userTokenPath() {
669
+ return join3(homedir3(), ".config", "fluncle", `user.${activeProfile()}.json`);
670
+ }
671
+ function isStoredUserToken(value) {
672
+ if (typeof value !== "object" || value === null) {
673
+ return false;
674
+ }
675
+ const record = value;
676
+ return typeof record.token === "string" && typeof record.baseUrl === "string";
677
+ }
678
+ function readUserToken() {
679
+ try {
680
+ const parsed = JSON.parse(readFileSync(userTokenPath(), "utf8"));
681
+ return isStoredUserToken(parsed) ? parsed : undefined;
682
+ } catch {
683
+ return;
684
+ }
685
+ }
686
+ function writeUserToken(value) {
687
+ const path2 = userTokenPath();
688
+ mkdirSync(dirname2(path2), { recursive: true });
689
+ writeFileSync(path2, `${JSON.stringify(value, null, 2)}
690
+ `, { mode: 384 });
691
+ }
692
+ function clearUserToken() {
693
+ const path2 = userTokenPath();
694
+ try {
695
+ readFileSync(path2);
696
+ } catch {
697
+ return false;
698
+ }
699
+ rmSync(path2, { force: true });
700
+ return true;
701
+ }
702
+ var envProfiles3, defaultEnvProfile2 = "production";
703
+ var init_user_token = __esm(() => {
704
+ envProfiles3 = ["local", "production"];
705
+ });
706
+
660
707
  // src/api.ts
708
+ async function userApiGet(path2) {
709
+ return apiRequest(path2, {
710
+ headers: userHeaders()
711
+ });
712
+ }
713
+ function userHeaders() {
714
+ const stored = readUserToken();
715
+ if (!stored) {
716
+ throw new CliError2("not_logged_in", "You're not signed in. Run `fluncle login` to link this device to your account.");
717
+ }
718
+ return {
719
+ Authorization: `Bearer ${stored.token}`
720
+ };
721
+ }
661
722
  async function publicApiGet(path2) {
662
723
  return apiRequest(path2);
663
724
  }
@@ -747,6 +808,7 @@ function parseJson(text) {
747
808
  var init_api = __esm(() => {
748
809
  init_env();
749
810
  init_output();
811
+ init_user_token();
750
812
  });
751
813
 
752
814
  // src/commands/recent.ts
@@ -1163,7 +1225,7 @@ __export(exports_mixtapes, {
1163
1225
  mixtapeDeleteCommand: () => mixtapeDeleteCommand,
1164
1226
  mixtapeCreateCommand: () => mixtapeCreateCommand
1165
1227
  });
1166
- import { existsSync, readFileSync } from "node:fs";
1228
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1167
1229
  async function mixtapesCommand() {
1168
1230
  const response = await publicApiGet("/api/mixtapes");
1169
1231
  return response.mixtapes;
@@ -1294,7 +1356,7 @@ function parseCueFile(filePath) {
1294
1356
  if (!existsSync(filePath)) {
1295
1357
  throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
1296
1358
  }
1297
- const text = readFileSync(filePath, "utf-8");
1359
+ const text = readFileSync2(filePath, "utf-8");
1298
1360
  const trimmed = text.trim();
1299
1361
  if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
1300
1362
  try {
@@ -1505,6 +1567,36 @@ var init_interactive = __esm(() => {
1505
1567
  // src/links.ts
1506
1568
  var spotifyPlaylistUrl2 = "https://open.spotify.com/playlist/1m5LADqpLjiBERdtqrIiL0", telegramUrl2 = "https://t.me/fluncle";
1507
1569
 
1570
+ // src/open-external.ts
1571
+ async function openExternal(target) {
1572
+ const command = platformOpenCommand();
1573
+ if (!command) {
1574
+ console.log(target);
1575
+ throw new CliError2("unsupported_platform", `Automatic opening is only supported on macOS and Linux. Open this manually: ${target}`);
1576
+ }
1577
+ const child = Bun.spawn([command, target], {
1578
+ stderr: "pipe",
1579
+ stdout: "pipe"
1580
+ });
1581
+ const exitCode = await child.exited;
1582
+ if (exitCode !== 0) {
1583
+ const stderr = await new Response(child.stderr).text();
1584
+ throw new CliError2("open_failed", stderr.trim() || `Could not open ${target} with ${command}`);
1585
+ }
1586
+ }
1587
+ function platformOpenCommand() {
1588
+ if (process.platform === "darwin") {
1589
+ return "open";
1590
+ }
1591
+ if (process.platform === "linux") {
1592
+ return "xdg-open";
1593
+ }
1594
+ return;
1595
+ }
1596
+ var init_open_external = __esm(() => {
1597
+ init_output();
1598
+ });
1599
+
1508
1600
  // src/commands/recent.ts
1509
1601
  function mapTrack2(track) {
1510
1602
  if (track.type === "mixtape") {
@@ -1598,31 +1690,6 @@ function spotifyPlaylistUrlToAppUri(playlistUrl) {
1598
1690
  }
1599
1691
  return `spotify:playlist:${playlistId}`;
1600
1692
  }
1601
- async function openExternal(target) {
1602
- const command = platformOpenCommand();
1603
- if (!command) {
1604
- console.log(target);
1605
- throw new CliError2("unsupported_platform", `Automatic opening is only supported on macOS and Linux. Open this manually: ${target}`);
1606
- }
1607
- const child = Bun.spawn([command, target], {
1608
- stderr: "pipe",
1609
- stdout: "pipe"
1610
- });
1611
- const exitCode = await child.exited;
1612
- if (exitCode !== 0) {
1613
- const stderr = await new Response(child.stderr).text();
1614
- throw new CliError2("open_failed", stderr.trim() || `Could not open ${target} with ${command}`);
1615
- }
1616
- }
1617
- function platformOpenCommand() {
1618
- if (process.platform === "darwin") {
1619
- return "open";
1620
- }
1621
- if (process.platform === "linux") {
1622
- return "xdg-open";
1623
- }
1624
- return;
1625
- }
1626
1693
  async function selectRecentTrack(tracks) {
1627
1694
  return await selectWithKeyboard(tracks, {
1628
1695
  nonInteractiveMessage: SELECT_NON_INTERACTIVE_MESSAGE,
@@ -1649,6 +1716,7 @@ var TELEGRAM_APP_URI = "tg://resolve?domain=fluncle", SPOTIFY_PLAYLIST_OPEN_URL,
1649
1716
  var init_open = __esm(() => {
1650
1717
  init_format();
1651
1718
  init_interactive();
1719
+ init_open_external();
1652
1720
  init_output();
1653
1721
  init_recent2();
1654
1722
  SPOTIFY_PLAYLIST_OPEN_URL = `${spotifyPlaylistUrl2}?si=054d3c6cbcf14a36`;
@@ -1756,6 +1824,162 @@ var init_submit = __esm(() => {
1756
1824
  init_output();
1757
1825
  });
1758
1826
 
1827
+ // src/commands/login.ts
1828
+ var exports_login = {};
1829
+ __export(exports_login, {
1830
+ logoutCommand: () => logoutCommand,
1831
+ loginCommand: () => loginCommand
1832
+ });
1833
+ async function loginCommand() {
1834
+ const baseUrl = getApiBaseUrl();
1835
+ const existing = readUserToken();
1836
+ if (existing) {
1837
+ const who2 = existing.user?.username ?? existing.user?.name ?? "your account";
1838
+ console.log(`Already signed in as ${who2}. Run \`fluncle logout\` first to switch accounts.`);
1839
+ return;
1840
+ }
1841
+ const device = await requestDeviceCode(baseUrl);
1842
+ const verificationUrl = device.verification_uri_complete ?? device.verification_uri;
1843
+ console.log(`Linking this device to your Fluncle account.
1844
+ `);
1845
+ console.log(` Code: ${device.user_code}`);
1846
+ console.log(` Open: ${verificationUrl}
1847
+ `);
1848
+ console.log("Opening your browser to approve… (sign in if you aren't already)");
1849
+ try {
1850
+ await openExternal(verificationUrl);
1851
+ } catch {
1852
+ console.log("Couldn't open a browser automatically — visit the URL above to approve.");
1853
+ }
1854
+ const token = await pollForToken(baseUrl, device);
1855
+ const user = await fetchSessionUser(baseUrl, token.access_token);
1856
+ writeUserToken({
1857
+ baseUrl,
1858
+ token: token.access_token,
1859
+ user
1860
+ });
1861
+ const who = user?.username ?? user?.name ?? "cosmonaut";
1862
+ console.log(`
1863
+ Aboard, ${who}. This device is linked to your Galaxy.`);
1864
+ console.log("Try `fluncle me` to see your progress.");
1865
+ }
1866
+ async function logoutCommand() {
1867
+ const stored = readUserToken();
1868
+ if (stored) {
1869
+ await revokeSession(stored.baseUrl, stored.token).catch(() => {
1870
+ return;
1871
+ });
1872
+ }
1873
+ const cleared = clearUserToken();
1874
+ console.log(cleared ? "Signed out. This device is no longer linked to your account." : "Not signed in — nothing to do.");
1875
+ }
1876
+ async function requestDeviceCode(baseUrl) {
1877
+ const response = await fetch(`${baseUrl}/api/auth/device/code`, {
1878
+ body: JSON.stringify({ client_id: CLI_CLIENT_ID, scope: DEVICE_SCOPE }),
1879
+ headers: { "Content-Type": "application/json" },
1880
+ method: "POST"
1881
+ });
1882
+ const data = await response.json().catch(() => {
1883
+ return;
1884
+ });
1885
+ if (!response.ok || !data?.device_code || !data.user_code) {
1886
+ throw new CliError2("device_code_failed", `Couldn't start the login flow (${response.status}). Try again in a moment.`);
1887
+ }
1888
+ return data;
1889
+ }
1890
+ async function pollForToken(baseUrl, device) {
1891
+ let intervalMs = Math.max(device.interval, 1) * 1000;
1892
+ const deadline = Date.now() + Math.max(device.expires_in, 1) * 1000;
1893
+ await sleep(intervalMs);
1894
+ while (Date.now() < deadline) {
1895
+ const response = await fetch(`${baseUrl}/api/auth/device/token`, {
1896
+ body: JSON.stringify({
1897
+ client_id: CLI_CLIENT_ID,
1898
+ device_code: device.device_code,
1899
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1900
+ }),
1901
+ headers: { "Content-Type": "application/json" },
1902
+ method: "POST"
1903
+ });
1904
+ const data = await response.json().catch(() => {
1905
+ return;
1906
+ });
1907
+ if (response.ok && data && "access_token" in data && data.access_token) {
1908
+ return data;
1909
+ }
1910
+ const pending = data && "error" in data ? data : undefined;
1911
+ switch (pending?.error) {
1912
+ case "authorization_pending":
1913
+ break;
1914
+ case "slow_down":
1915
+ intervalMs += 5000;
1916
+ break;
1917
+ case "access_denied":
1918
+ throw new CliError2("access_denied", "Sign-in was denied in the browser. Nothing linked.");
1919
+ case "expired_token":
1920
+ throw new CliError2("expired_token", "The login request timed out. Run `fluncle login` again.");
1921
+ default:
1922
+ throw new CliError2("device_token_failed", pending?.error_description ?? `Login failed (${response.status}).`);
1923
+ }
1924
+ await sleep(intervalMs);
1925
+ }
1926
+ throw new CliError2("expired_token", "The login request timed out. Run `fluncle login` again.");
1927
+ }
1928
+ async function fetchSessionUser(baseUrl, token) {
1929
+ const response = await fetch(`${baseUrl}/api/me`, {
1930
+ headers: { Authorization: `Bearer ${token}` }
1931
+ });
1932
+ if (!response.ok) {
1933
+ return;
1934
+ }
1935
+ const data = await response.json().catch(() => {
1936
+ return;
1937
+ });
1938
+ return data?.user ?? undefined;
1939
+ }
1940
+ async function revokeSession(baseUrl, token) {
1941
+ await fetch(`${baseUrl}/api/auth/sign-out`, {
1942
+ headers: { Authorization: `Bearer ${token}` },
1943
+ method: "POST"
1944
+ });
1945
+ }
1946
+ function sleep(ms) {
1947
+ return new Promise((resolve) => setTimeout(resolve, ms));
1948
+ }
1949
+ var CLI_CLIENT_ID = "fluncle-cli", DEVICE_SCOPE = "galaxy-sync";
1950
+ var init_login = __esm(() => {
1951
+ init_env();
1952
+ init_open_external();
1953
+ init_output();
1954
+ init_user_token();
1955
+ });
1956
+
1957
+ // src/commands/me.ts
1958
+ var exports_me = {};
1959
+ __export(exports_me, {
1960
+ meCommand: () => meCommand
1961
+ });
1962
+ async function meCommand() {
1963
+ const [me, progress] = await Promise.all([
1964
+ userApiGet("/api/me"),
1965
+ userApiGet("/api/me/galaxy-progress")
1966
+ ]);
1967
+ if (!me.user) {
1968
+ throw new Error("Your sign-in expired. Run `fluncle login` to link this device again.");
1969
+ }
1970
+ return {
1971
+ collectedCount: progress.collectedLogIds.length,
1972
+ deaths: progress.deaths,
1973
+ joinedAt: me.user.createdAt,
1974
+ name: me.user.displayUsername ?? me.user.username ?? "cosmonaut",
1975
+ userId: me.user.id,
1976
+ wins: progress.wins
1977
+ };
1978
+ }
1979
+ var init_me = __esm(() => {
1980
+ init_api();
1981
+ });
1982
+
1759
1983
  // src/brand.ts
1760
1984
  var fluncleAsciiLogo2 = `███████╗██╗ ██╗ ██╗███╗ ██╗ ██████╗██╗ ███████╗
1761
1985
  ██╔════╝██║ ██║ ██║████╗ ██║██╔════╝██║ ██╔════╝
@@ -1914,7 +2138,7 @@ function parseVersion2(version) {
1914
2138
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
1915
2139
  var init_version2 = __esm(() => {
1916
2140
  init_output();
1917
- currentVersion2 = "0.65.0".trim() ? "0.65.0".trim() : "0.1.0";
2141
+ currentVersion2 = "0.66.0".trim() ? "0.66.0".trim() : "0.1.0";
1918
2142
  });
1919
2143
 
1920
2144
  // ../../packages/registry/src/index.ts
@@ -3280,7 +3504,7 @@ __export(exports_newsletter, {
3280
3504
  newsletterListCommand: () => newsletterListCommand,
3281
3505
  newsletterDraftCommand: () => newsletterDraftCommand
3282
3506
  });
3283
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
3507
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
3284
3508
  function buildBody2(options, { requireContent }) {
3285
3509
  const body = {};
3286
3510
  if (options.contentFile !== undefined) {
@@ -3303,7 +3527,7 @@ function readContentFile(filePath) {
3303
3527
  if (!existsSync2(filePath)) {
3304
3528
  throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
3305
3529
  }
3306
- const text = readFileSync2(filePath, "utf-8");
3530
+ const text = readFileSync3(filePath, "utf-8");
3307
3531
  try {
3308
3532
  return JSON.parse(text);
3309
3533
  } catch (error) {
@@ -3937,7 +4161,7 @@ var COORD_FALLBACK2 = "—";
3937
4161
  var init_format2 = () => {};
3938
4162
 
3939
4163
  // src/cli.ts
3940
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4164
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
3941
4165
  import path2 from "path";
3942
4166
 
3943
4167
  // ../../node_modules/commander/lib/error.js
@@ -6057,6 +6281,7 @@ ${fluncleAsciiLogo}
6057
6281
  `).addHelpText("after", rootHelpSections);
6058
6282
  addListenCommands(program2);
6059
6283
  addShareCommands(program2);
6284
+ addAccountCommands(program2);
6060
6285
  addMetaCommands(program2);
6061
6286
  addTrackCommands(program2);
6062
6287
  addAdminCommands(program2);
@@ -6120,6 +6345,20 @@ function addShareCommands(program2) {
6120
6345
  await submitCommand2(input.join(" ") || undefined);
6121
6346
  });
6122
6347
  }
6348
+ function addAccountCommands(program2) {
6349
+ program2.command("login").description("Link this device to your Fluncle account (sync your Galaxy)").action(async () => {
6350
+ const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), exports_login));
6351
+ await loginCommand2();
6352
+ });
6353
+ program2.command("logout").description("Unlink this device from your account").action(async () => {
6354
+ const { logoutCommand: logoutCommand2 } = await Promise.resolve().then(() => (init_login(), exports_login));
6355
+ await logoutCommand2();
6356
+ });
6357
+ program2.command("me").description("Your account and Galaxy progress (sign in with `fluncle login`)").option("--json", "Print JSON", false).action(async (options) => {
6358
+ const { meCommand: meCommand2 } = await Promise.resolve().then(() => (init_me(), exports_me));
6359
+ await runMe(options, meCommand2);
6360
+ });
6361
+ }
6123
6362
  function addMetaCommands(program2) {
6124
6363
  program2.command("about").description("Fluncle, and where to find him").action(async () => {
6125
6364
  const { aboutCommand: aboutCommand2 } = await Promise.resolve().then(() => (init_about(), exports_about));
@@ -6389,7 +6628,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
6389
6628
  console.log(` mime: ${result.mime}`);
6390
6629
  }
6391
6630
  async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
6392
- const script = options.scriptFile ? readFileSync3(options.scriptFile, "utf8") : options.script;
6631
+ const script = options.scriptFile ? readFileSync4(options.scriptFile, "utf8") : options.script;
6393
6632
  if (!idOrLogId || !script || !script.trim()) {
6394
6633
  throw new Error("Usage: fluncle admin tracks observe <track_id|log_id> (--script <text> | --script-file <file>) [--voice-id <id>] [--duration-ms <ms>] [--context-note <text>] [--json]");
6395
6634
  }
@@ -6445,7 +6684,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
6445
6684
  }
6446
6685
  }
6447
6686
  async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
6448
- const note = options.scriptFile ? readFileSync3(options.scriptFile, "utf8") : options.script;
6687
+ const note = options.scriptFile ? readFileSync4(options.scriptFile, "utf8") : options.script;
6449
6688
  if (!idOrLogId || !note || !note.trim()) {
6450
6689
  throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
6451
6690
  }
@@ -7200,6 +7439,20 @@ async function runStatus(options, statusCommand2) {
7200
7439
  console.log(statusLines2(snapshot).join(`
7201
7440
  `));
7202
7441
  }
7442
+ async function runMe(options, meCommand2) {
7443
+ const me = await meCommand2();
7444
+ if (options.json) {
7445
+ printJson({ ok: true, ...me });
7446
+ return;
7447
+ }
7448
+ console.log(me.name);
7449
+ console.log([
7450
+ `${me.collectedCount} ${me.collectedCount === 1 ? "finding" : "findings"} collected`,
7451
+ `${me.wins} ${me.wins === 1 ? "win" : "wins"}`,
7452
+ `${me.deaths} ${me.deaths === 1 ? "death" : "deaths"}`
7453
+ ].join(" \xB7 "));
7454
+ console.log(`Aboard since ${new Date(me.joinedAt).toLocaleDateString()}`);
7455
+ }
7203
7456
  function rejectUnexpectedPositionals(positionals) {
7204
7457
  if (positionals.length === 0) {
7205
7458
  return;
package/package.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.65.0"
34
+ "version": "0.66.0"
35
35
  }