fluncle 0.200.0 → 0.202.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 +142 -77
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -367,6 +367,55 @@ var require_main = __commonJS((exports, module) => {
367
367
  module.exports = DotenvModule;
368
368
  });
369
369
 
370
+ // src/env.ts
371
+ import { homedir } from "node:os";
372
+ import { join } from "node:path";
373
+ function loadEnv(keys) {
374
+ loadConfig();
375
+ const result = {};
376
+ const missing = [];
377
+ for (const key of keys) {
378
+ const value = process.env[key];
379
+ if (value === undefined) {
380
+ missing.push(key);
381
+ continue;
382
+ }
383
+ result[key] = value;
384
+ }
385
+ if (missing.length > 0) {
386
+ throw new Error(`Missing required env vars: ${missing.join(", ")}`);
387
+ }
388
+ return result;
389
+ }
390
+ function getApiBaseUrl() {
391
+ loadConfig();
392
+ return (process.env.FLUNCLE_API_BASE_URL ?? "https://www.fluncle.com").replace(/\/+$/, "");
393
+ }
394
+ function loadConfig() {
395
+ const profile = getEnvProfile();
396
+ if (loadedProfile === profile) {
397
+ return;
398
+ }
399
+ loadedProfile = profile;
400
+ if (false) {}
401
+ import_dotenv2.config({ path: join(homedir(), `.config/fluncle/.env.${profile}`), quiet: true });
402
+ }
403
+ function getEnvProfile() {
404
+ const profile = process.env.FLUNCLE_ENV ?? defaultEnvProfile;
405
+ if (!isEnvProfile2(profile)) {
406
+ throw new Error(`Unknown env profile: ${profile}. Expected local or production.`);
407
+ }
408
+ return profile;
409
+ }
410
+ function isEnvProfile2(value) {
411
+ return envProfiles2.includes(value);
412
+ }
413
+ var import_dotenv2, envProfiles2, defaultEnvProfile = "production", loadedProfile;
414
+ var init_env = __esm(() => {
415
+ import_dotenv2 = __toESM(require_main(), 1);
416
+ envProfiles2 = ["local", "production"];
417
+ });
418
+
370
419
  // ../../packages/contracts/src/util.ts
371
420
  function formatDuration(durationMs) {
372
421
  const totalSeconds = Math.round(durationMs / 1000);
@@ -561,7 +610,7 @@ function parseVersion(version) {
561
610
  var currentVersion;
562
611
  var init_version = __esm(() => {
563
612
  init_output();
564
- currentVersion = "0.200.0".trim() ? "0.200.0".trim() : "0.1.0";
613
+ currentVersion = "0.202.0".trim() ? "0.202.0".trim() : "0.1.0";
565
614
  });
566
615
 
567
616
  // src/update-notifier.ts
@@ -573,8 +622,8 @@ __export(exports_update_notifier, {
573
622
  detectInstallMethod: () => detectInstallMethod
574
623
  });
575
624
  import { mkdir, readFile, writeFile } from "node:fs/promises";
576
- import { homedir, platform } from "node:os";
577
- import { dirname, join, sep } from "node:path";
625
+ import { homedir as homedir2, platform } from "node:os";
626
+ import { dirname, join as join2, sep } from "node:path";
578
627
  import { fileURLToPath } from "node:url";
579
628
  async function notifyIfUpdateAvailable(args) {
580
629
  try {
@@ -686,8 +735,8 @@ async function writeCache(state) {
686
735
  } catch {}
687
736
  }
688
737
  function cacheFilePath() {
689
- const base = process.env.XDG_CACHE_HOME?.trim() || platform() === "win32" && process.env.LOCALAPPDATA?.trim() || join(homedir(), ".config");
690
- return join(base, "fluncle", "update-check.json");
738
+ const base = process.env.XDG_CACHE_HOME?.trim() || platform() === "win32" && process.env.LOCALAPPDATA?.trim() || join2(homedir2(), ".config");
739
+ return join2(base, "fluncle", "update-check.json");
691
740
  }
692
741
  function buildNotice(current, latest) {
693
742
  const method = detectInstallMethod({ entry: entryPath(), execPath: process.execPath || "" });
@@ -747,55 +796,6 @@ var init_update_notifier = __esm(() => {
747
796
  cacheTtlMs = 24 * 60 * 60 * 1000;
748
797
  });
749
798
 
750
- // src/env.ts
751
- import { homedir as homedir2 } from "node:os";
752
- import { join as join2 } from "node:path";
753
- function loadEnv(keys) {
754
- loadConfig();
755
- const result = {};
756
- const missing = [];
757
- for (const key of keys) {
758
- const value = process.env[key];
759
- if (value === undefined) {
760
- missing.push(key);
761
- continue;
762
- }
763
- result[key] = value;
764
- }
765
- if (missing.length > 0) {
766
- throw new Error(`Missing required env vars: ${missing.join(", ")}`);
767
- }
768
- return result;
769
- }
770
- function getApiBaseUrl() {
771
- loadConfig();
772
- return (process.env.FLUNCLE_API_BASE_URL ?? "https://www.fluncle.com").replace(/\/+$/, "");
773
- }
774
- function loadConfig() {
775
- const profile = getEnvProfile();
776
- if (loadedProfile === profile) {
777
- return;
778
- }
779
- loadedProfile = profile;
780
- if (false) {}
781
- import_dotenv2.config({ path: join2(homedir2(), `.config/fluncle/.env.${profile}`), quiet: true });
782
- }
783
- function getEnvProfile() {
784
- const profile = process.env.FLUNCLE_ENV ?? defaultEnvProfile;
785
- if (!isEnvProfile2(profile)) {
786
- throw new Error(`Unknown env profile: ${profile}. Expected local or production.`);
787
- }
788
- return profile;
789
- }
790
- function isEnvProfile2(value) {
791
- return envProfiles2.includes(value);
792
- }
793
- var import_dotenv2, envProfiles2, defaultEnvProfile = "production", loadedProfile;
794
- var init_env = __esm(() => {
795
- import_dotenv2 = __toESM(require_main(), 1);
796
- envProfiles2 = ["local", "production"];
797
- });
798
-
799
799
  // src/user-token.ts
800
800
  import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
801
801
  import { homedir as homedir3 } from "node:os";
@@ -1400,10 +1400,12 @@ async function resyncYoutube(mixtapeId) {
1400
1400
  }
1401
1401
  async function authYoutubeCommand() {
1402
1402
  const response = await adminApiGet("/api/v1/admin/youtube/auth/start");
1403
- console.log(`Open this YouTube authorization URL:
1403
+ console.log(`Open this link in the browser you're logged into /admin with:
1404
1404
 
1405
1405
  ${response.authUrl}
1406
1406
 
1407
+ It hands you off to Google. Signed out? Log in there and it resumes on its own.
1408
+ The link is good for 10 minutes; run this again if it expires.
1407
1409
  After approving access, Google returns to the Fluncle admin callback and stores the refresh token server-side.`);
1408
1410
  }
1409
1411
  async function initiate(mixtapeId, contentLength, contentType) {
@@ -1571,10 +1573,12 @@ async function resyncMixcloud(mixtapeId) {
1571
1573
  }
1572
1574
  async function authMixcloudCommand() {
1573
1575
  const response = await adminApiGet("/api/v1/admin/mixcloud/auth/start");
1574
- console.log(`Open this Mixcloud authorization URL:
1576
+ console.log(`Open this link in the browser you're logged into /admin with:
1575
1577
 
1576
1578
  ${response.authUrl}
1577
1579
 
1580
+ It hands you off to Mixcloud. Signed out? Log in there and it resumes on its own.
1581
+ The link is good for 10 minutes; run this again if it expires.
1578
1582
  After approving access, Mixcloud returns to the Fluncle admin callback and stores the access token server-side.`);
1579
1583
  }
1580
1584
  function mixtapeDescription(note, logId) {
@@ -2448,7 +2452,7 @@ function parseVersion2(version) {
2448
2452
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2449
2453
  var init_version2 = __esm(() => {
2450
2454
  init_output();
2451
- currentVersion2 = "0.200.0".trim() ? "0.200.0".trim() : "0.1.0";
2455
+ currentVersion2 = "0.202.0".trim() ? "0.202.0".trim() : "0.1.0";
2452
2456
  });
2453
2457
 
2454
2458
  // ../../packages/registry/src/index.ts
@@ -2517,18 +2521,6 @@ var init_src = __esm(() => {
2517
2521
  url: `${SITE}/mix`,
2518
2522
  weights: { web: "secondary" }
2519
2523
  },
2520
- {
2521
- exposedContent: [
2522
- "the feed-first Stories surface — full-bleed findings",
2523
- "/stories/:logId — one finding as a Story"
2524
- ],
2525
- kind: "web_route",
2526
- name: "web.stories",
2527
- probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2528
- route: "/stories",
2529
- url: `${SITE}/stories`,
2530
- weights: { web: "secondary" }
2531
- },
2532
2524
  {
2533
2525
  exposedContent: ["who Fluncle is, what the Galaxy is, how to read a Log ID"],
2534
2526
  kind: "web_route",
@@ -3113,7 +3105,7 @@ var init_src = __esm(() => {
3113
3105
  exposedContent: ["the XML sitemap index of every public page"],
3114
3106
  kind: "discovery",
3115
3107
  name: "discovery.sitemap",
3116
- operatorNotes: "A sitemap INDEX, not a flat urlset: the URLs live in children at /sitemap/<kind>-<n>.xml, ONE CHILD PER ENTITY TYPE (pages/findings/artists/labels/albums/galaxies/logbook), each auto-paged under Google's 50,000-URL ceiling so a breach cannot happen rather than merely not having happened yet. robots.txt still names this one URL — a crawler discovers the children from here. Splitting per entity type is also the diagnostic: Search Console reports coverage PER sitemap, so each entity type gets its own submitted/indexed count.",
3108
+ operatorNotes: "A sitemap INDEX, not a flat urlset: the URLs live in children at /sitemap/<kind>-<n>.xml, ONE CHILD PER ENTITY TYPE (pages/findings/artists/labels/albums/galaxies/logbook/docs), each auto-paged under Google's 50,000-URL ceiling so a breach cannot happen rather than merely not having happened yet. robots.txt still names this one URL — a crawler discovers the children from here. Splitting per entity type is also the diagnostic: Search Console reports coverage PER sitemap, so each entity type gets its own submitted/indexed count.",
3117
3109
  probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
3118
3110
  route: "/sitemap.xml",
3119
3111
  url: `${SITE}/sitemap.xml`,
@@ -3122,7 +3114,7 @@ var init_src = __esm(() => {
3122
3114
  {
3123
3115
  apiFormat: "application/xml",
3124
3116
  exposedContent: [
3125
- "one child sitemap, per entity type: the pages / findings / artists / labels / albums / galaxies / logbook URLs, auto-paged"
3117
+ "one child sitemap, per entity type: the pages / findings / artists / labels / albums / galaxies / logbook / docs URLs, auto-paged"
3126
3118
  ],
3127
3119
  kind: "discovery",
3128
3120
  name: "discovery.sitemap-shard",
@@ -3153,6 +3145,16 @@ var init_src = __esm(() => {
3153
3145
  url: `${SITE}/llms.txt`,
3154
3146
  weights: { web: "primary" }
3155
3147
  },
3148
+ {
3149
+ apiFormat: "text/markdown",
3150
+ exposedContent: ["one developer-doc page as clean Markdown, at `/docs.md/<slug>`"],
3151
+ kind: "discovery",
3152
+ name: "discovery.docs-markdown",
3153
+ operatorNotes: 'The Markdown twin of every /docs page — front-matter-free, the title as the H1, the description as the lede, then the precompiled body (`includeProcessedMarkdown` in source.config.ts → `page.data.getText("processed")`). It is the endpoint behind the page-actions affordance ("View as Markdown", "Copy page", and the Open in ChatGPT/Claude/Cursor links, which carry this URL so the assistant pulls the clean Markdown), and each /docs page advertises it as `<link rel="alternate" type="text/markdown">` (routes/-docs-head.ts). A machine-FETCHED document rather than a page, so it is catalogued as `discovery` next to llms.txt. No probeConfig: the route is slug-parameterized, so there is no fixed URL to GET-probe (the `discovery.oembed` / `web.artist` precedent) — the post-deploy probe skips it for the same reason it skips /artist/:slug/fresh.xml. Source: apps/web/src/routes/docs[.]md.$.ts (a pure splat, mounted at /docs.md/$).',
3154
+ route: "/docs.md/:slug",
3155
+ url: `${SITE}/docs.md/cli`,
3156
+ weights: { web: "tertiary" }
3157
+ },
3156
3158
  {
3157
3159
  apiFormat: "text/markdown",
3158
3160
  exposedContent: ["the entire archive in one ingestible markdown document, every finding"],
@@ -3256,7 +3258,7 @@ var init_src = __esm(() => {
3256
3258
  {
3257
3259
  command: "ssh rave.fluncle.com",
3258
3260
  exposedContent: [
3259
- "the rave terminal TUI: Latest bangers, Fresh releases, Artist archive, Sonic galaxies, Mixtape archive, Random banger, Submit, Subscribe, Install CLI, System status, About",
3261
+ "the rave terminal TUI: Latest findings, Fresh releases, Artist archive, Sonic galaxies, Mixtape archive, Random banger, Submit, Subscribe, Install CLI, System status, About",
3260
3262
  "deep-register one-shots: `ssh rave.fluncle.com latest|fresh|random`"
3261
3263
  ],
3262
3264
  kind: "ssh",
@@ -5054,10 +5056,12 @@ async function resyncYoutube2(mixtapeId) {
5054
5056
  }
5055
5057
  async function authYoutubeCommand2() {
5056
5058
  const response = await adminApiGet("/api/v1/admin/youtube/auth/start");
5057
- console.log(`Open this YouTube authorization URL:
5059
+ console.log(`Open this link in the browser you're logged into /admin with:
5058
5060
 
5059
5061
  ${response.authUrl}
5060
5062
 
5063
+ It hands you off to Google. Signed out? Log in there and it resumes on its own.
5064
+ The link is good for 10 minutes; run this again if it expires.
5061
5065
  After approving access, Google returns to the Fluncle admin callback and stores the refresh token server-side.`);
5062
5066
  }
5063
5067
  async function initiate2(mixtapeId, contentLength, contentType) {
@@ -6394,11 +6398,13 @@ __export(exports_auth, {
6394
6398
  });
6395
6399
  async function authSpotifyCommand() {
6396
6400
  const response = await adminApiGet("/api/v1/admin/spotify/auth/start");
6397
- console.log(`Open this Spotify authorization URL:
6401
+ console.log(`Open this link in the browser you're logged into /admin with:
6398
6402
 
6399
6403
  ${response.authUrl}
6400
6404
 
6401
- After approving access, Spotify will return to the Fluncle admin callback and store auth server-side.`);
6405
+ It hands you off to Spotify. Signed out? Log in there and it resumes on its own.
6406
+ The link is good for 10 minutes; run this again if it expires.
6407
+ After approving access, Spotify returns to the Fluncle admin callback and stores auth server-side.`);
6402
6408
  }
6403
6409
  var init_auth = __esm(() => {
6404
6410
  init_api();
@@ -6475,10 +6481,12 @@ async function resyncMixcloud2(mixtapeId) {
6475
6481
  }
6476
6482
  async function authMixcloudCommand2() {
6477
6483
  const response = await adminApiGet("/api/v1/admin/mixcloud/auth/start");
6478
- console.log(`Open this Mixcloud authorization URL:
6484
+ console.log(`Open this link in the browser you're logged into /admin with:
6479
6485
 
6480
6486
  ${response.authUrl}
6481
6487
 
6488
+ It hands you off to Mixcloud. Signed out? Log in there and it resumes on its own.
6489
+ The link is good for 10 minutes; run this again if it expires.
6482
6490
  After approving access, Mixcloud returns to the Fluncle admin callback and stores the access token server-side.`);
6483
6491
  }
6484
6492
  function mixtapeDescription2(note, logId) {
@@ -6554,10 +6562,12 @@ __export(exports_auth_tiktok, {
6554
6562
  });
6555
6563
  async function authTikTokCommand() {
6556
6564
  const response = await adminApiGet("/api/v1/admin/tiktok/auth/start");
6557
- console.log(`Open this TikTok authorization URL:
6565
+ console.log(`Open this link in the browser you're logged into /admin with:
6558
6566
 
6559
6567
  ${response.authUrl}
6560
6568
 
6569
+ It hands you off to TikTok. Signed out? Log in there and it resumes on its own.
6570
+ The link is good for 10 minutes; run this again if it expires.
6561
6571
  After approving access, TikTok returns to the Fluncle admin callback and stores the refresh token server-side.`);
6562
6572
  }
6563
6573
  var init_auth_tiktok = __esm(() => {
@@ -9352,6 +9362,60 @@ function isEnvProfile(value) {
9352
9362
  var spotifyPlaylistUrl = "https://open.spotify.com/playlist/1m5LADqpLjiBERdtqrIiL0";
9353
9363
  var telegramUrl = "https://t.me/fluncle";
9354
9364
 
9365
+ // src/live.ts
9366
+ init_env();
9367
+ var NEBULA_VIOLET = "\x1B[38;2;171;123;255m";
9368
+ var RESET = "\x1B[0m";
9369
+ var TIMEOUT_MS = 1500;
9370
+ function shouldSkip(args) {
9371
+ if (process.env.FLUNCLE_NO_LIVE_CALLOUT === "1") {
9372
+ return true;
9373
+ }
9374
+ if (process.env.CI) {
9375
+ return true;
9376
+ }
9377
+ if (args.length === 0) {
9378
+ return true;
9379
+ }
9380
+ const first = args[0];
9381
+ if (first === "admin" || first === "help") {
9382
+ return true;
9383
+ }
9384
+ return args.some((arg) => arg === "--json" || arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V");
9385
+ }
9386
+ function displayUrl(url) {
9387
+ return url.replace(/^https?:\/\/(www\.)?/, "");
9388
+ }
9389
+ async function maybePrintLiveCallout(args) {
9390
+ if (shouldSkip(args) || process.stdout.isTTY !== true) {
9391
+ return;
9392
+ }
9393
+ try {
9394
+ const controller = new AbortController;
9395
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
9396
+ let data;
9397
+ try {
9398
+ const response = await fetch(`${getApiBaseUrl()}/api/v1/status`, {
9399
+ signal: controller.signal
9400
+ });
9401
+ if (!response.ok) {
9402
+ return;
9403
+ }
9404
+ data = await response.json();
9405
+ } finally {
9406
+ clearTimeout(timer);
9407
+ }
9408
+ const live = data.live;
9409
+ if (!live?.on) {
9410
+ return;
9411
+ }
9412
+ const text = `On the decks, live now: ${displayUrl(live.url)}`;
9413
+ const line = process.env.NO_COLOR ? text : `${NEBULA_VIOLET}${text}${RESET}`;
9414
+ console.log(`${line}
9415
+ `);
9416
+ } catch {}
9417
+ }
9418
+
9355
9419
  // src/output.ts
9356
9420
  import { writeSync } from "node:fs";
9357
9421
  var EAGAIN_WAIT = new Int32Array(new SharedArrayBuffer(4));
@@ -9430,6 +9494,7 @@ async function main(args = process.argv.slice(2)) {
9430
9494
  program2.outputHelp();
9431
9495
  return;
9432
9496
  }
9497
+ await maybePrintLiveCallout(args);
9433
9498
  try {
9434
9499
  assertParseArgsCompatiblePositionals(args);
9435
9500
  assertParseArgsCompatibleOptionValues(args);
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.200.0"
34
+ "version": "0.202.0"
35
35
  }