yanki 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- var Y=Object.defineProperty;var m=(e,t)=>Y(e,"name",{value:t,configurable:!0});import{y as J,u as h,k as T,i as D,l as I,b as U,c as L,f as M,s as z,g as H}from"../sync-CNwtrBlB.js";import s from"chalk";import{globby as j}from"globby";import F from"node:fs/promises";import W from"node:path";import q from"node:os";import B from"yargs";import{hideBin as G}from"yargs/helpers";import"rehype-mathjax";import"rehype-parse";import"node:crypto";var K="0.4.1";const N=process?.versions?.node!==void 0,d={verbose:!1,log(...e){if(!this.verbose)return;const t=s.gray("[Log]");N?console.warn(t,...e):console.log(t,...e)},logPrefixed(e,...t){this.info(s.blue(`[${e}]`),...t)},info(...e){if(!this.verbose)return;const t=s.green("[Info]");N?console.warn(t,...e):console.info(t,...e)},infoPrefixed(e,...t){this.info(s.blue(`[${e}]`),...t)},warn(...e){console.warn(s.yellow("[Warning]"),...e)},warnPrefixed(e,...t){this.warn(s.blue(`[${e}]`),...t)},error(...e){console.error(s.red("[Error]"),...e)},errorPrefixed(e,...t){this.error(s.blue(`[${e}]`),...t)}},y={"anki-auto-launch":{alias:"l",default:!1,describe:"Attempt to open the Anki desktop app if it's not already running. (Experimental, macOS only.)",type:"boolean"}},b={"anki-web":{alias:"w",default:!0,describe:'Automatically sync any changes to AnkiWeb after Yanki has finished syncing locally. If false, only local Anki data is updated and you must manually invoke a sync to AnkiWeb. This is the equivalent of pushing the "sync" button in the Anki app.',type:"boolean"}},w={"anki-connect":{default:"http://127.0.0.1:8765",describe:"Host and port of the Anki-Connect server. The default is usually fine. See the Anki-Connect documentation for more information.",type:"string"}},v={verbose:{default:!1,describe:"Enable verbose logging.",type:"boolean"}};function g(e){return{json:{default:!1,describe:e,type:"boolean"}}}m(g,"jsonOption");const S={"dry-run":{alias:"d",default:!1,describe:"Run without making any changes to the Anki database. See a report of what would have been done.",type:"boolean"}};function A(e){return{namespace:{alias:"n",default:J,describe:e,type:"string"}}}m(A,"namespaceOption");const x=q.homedir();function Q(e){if(typeof e!="string")throw new TypeError(`Expected a string, got ${typeof e}`);return x?e.replace(/^~(?=$|\/|\\)/,x):e}m(Q,"untildify");const k=m(e=>{const{code:t}=e.cause;throw t==="ECONNREFUSED"&&(d.error("Failed to connect to Anki. Make sure Anki is running and AnkiConnect is installed."),process.exitCode=1,process.exit()),e instanceof Error?e:new Error("Unknown error")},"ankiNotRunningErrorHandler"),R=B(G(process.argv));await R.scriptName("yanki").usage("$0 [command]","Run a Yanki command. Defaults to `sync` if a command is not provided.").command(["$0 <directory> [options]","sync <directory> [options]"],"Perform a one-way synchronization from a local directory of Markdown files to the Anki database. Any Markdown files in subdirectories are included as well.",e=>e.positional("directory",{demandOption:!0,describe:"The path to the local directory of Markdown files to sync.",type:"string"}).option(S).option(A("Advanced option for managing multiple Yanki synchronization groups. Case insensitive. See the readme for more information.")).option(w).option(y).option(b).option("manage-filenames",{alias:"m",choices:["off","prompt","response"],default:"off",describe:'Rename local note files to match their content. Useful if you want to feel have semantically reasonable note file names without managing them by hand. The `"prompt"` option will attempt to create the filename based on the "front" of the card, while `"response"` will prioritize the "back", "Cloze", or "type in the answer" portions of the card. Truncation, sanitization, and deduplication are taken care of.',type:"string"}).option("max-filename-length",{default:void 0,defaultDescription:"60",describe:"If `manage-filenames` is enabled, this option specifies the maximum length of the filename in characters.",type:"number"}).option(g("Output the sync report as JSON.")).option(v),async({ankiAutoLaunch:e,ankiConnect:t,ankiWeb:i,directory:o,dryRun:a,json:r,manageFilenames:n,maxFilenameLength:p,namespace:f,recursive:c=!0,verbose:u})=>{d.verbose=u;const l=Q(o),E=c?`${l}/**/*.md`:`${l}/*.md`,C=await j(E,{absolute:!0});if(C.length===0){d.error(`No Markdown files found in "${l}".`),process.exitCode=1;return}n==="off"&&p!==void 0&&d.warn("Ignoring `max-filename-length` option because `manage-filenames` is not enabled.");const{host:$,port:P}=h(t),O=await T(C,{ankiConnectOptions:{autoLaunch:e,host:$,port:P},ankiWeb:i,dryRun:a,manageFilenames:n,maxFilenameLength:p,namespace:f}).catch(k);r?(process.stdout.write(JSON.stringify(O,void 0,2)),process.stdout.write(`
2
+ var Y=Object.defineProperty;var m=(e,t)=>Y(e,"name",{value:t,configurable:!0});import{y as J,u as h,k as T,j as D,l as I,b as U,c as L,f as M,s as z,h as j}from"../sync-CnK6Mxek.js";import s from"chalk";import{globby as H}from"globby";import F from"node:fs/promises";import W from"node:path";import q from"node:os";import B from"yargs";import{hideBin as G}from"yargs/helpers";import"rehype-mathjax";import"rehype-parse";import"node:crypto";var K="0.5.0";const N=process?.versions?.node!==void 0,d={verbose:!1,log(...e){if(!this.verbose)return;const t=s.gray("[Log]");N?console.warn(t,...e):console.log(t,...e)},logPrefixed(e,...t){this.info(s.blue(`[${e}]`),...t)},info(...e){if(!this.verbose)return;const t=s.green("[Info]");N?console.warn(t,...e):console.info(t,...e)},infoPrefixed(e,...t){this.info(s.blue(`[${e}]`),...t)},warn(...e){console.warn(s.yellow("[Warning]"),...e)},warnPrefixed(e,...t){this.warn(s.blue(`[${e}]`),...t)},error(...e){console.error(s.red("[Error]"),...e)},errorPrefixed(e,...t){this.error(s.blue(`[${e}]`),...t)}},y={"anki-auto-launch":{alias:"l",default:!1,describe:"Attempt to open the Anki desktop app if it's not already running. (Experimental, macOS only.)",type:"boolean"}},g={"anki-web":{alias:"w",default:!0,describe:'Automatically sync any changes to AnkiWeb after Yanki has finished syncing locally. If false, only local Anki data is updated and you must manually invoke a sync to AnkiWeb. This is the equivalent of pushing the "sync" button in the Anki app.',type:"boolean"}},w={"anki-connect":{default:"http://127.0.0.1:8765",describe:"Host and port of the Anki-Connect server. The default is usually fine. See the Anki-Connect documentation for more information.",type:"string"}},v={verbose:{default:!1,describe:"Enable verbose logging.",type:"boolean"}};function k(e){return{json:{default:!1,describe:e,type:"boolean"}}}m(k,"jsonOption");const S={"dry-run":{alias:"d",default:!1,describe:"Run without making any changes to the Anki database. See a report of what would have been done.",type:"boolean"}};function A(e){return{namespace:{alias:"n",default:J,describe:e,type:"string"}}}m(A,"namespaceOption");const x=q.homedir();function Q(e){if(typeof e!="string")throw new TypeError(`Expected a string, got ${typeof e}`);return x?e.replace(/^~(?=$|\/|\\)/,x):e}m(Q,"untildify");const b=m(e=>{const{code:t}=e.cause;throw t==="ECONNREFUSED"&&(d.error("Failed to connect to Anki. Make sure Anki is running and AnkiConnect is installed."),process.exitCode=1,process.exit()),e instanceof Error?e:new Error("Unknown error")},"ankiNotRunningErrorHandler"),R=B(G(process.argv));await R.scriptName("yanki").usage("$0 [command]","Run a Yanki command. Defaults to `sync` if a command is not provided.").command(["$0 <directory> [options]","sync <directory> [options]"],"Perform a one-way synchronization from a local directory of Markdown files to the Anki database. Any Markdown files in subdirectories are included as well.",e=>e.positional("directory",{demandOption:!0,describe:"The path to the local directory of Markdown files to sync.",type:"string"}).option(S).option(A("Advanced option for managing multiple Yanki synchronization groups. Case insensitive. See the readme for more information.")).option(w).option(y).option(g).option("manage-filenames",{alias:"m",choices:["off","prompt","response"],default:"off",describe:'Rename local note files to match their content. Useful if you want to feel have semantically reasonable note file names without managing them by hand. The `"prompt"` option will attempt to create the filename based on the "front" of the card, while `"response"` will prioritize the "back", "Cloze", or "type in the answer" portions of the card. Truncation, sanitization, and deduplication are taken care of.',type:"string"}).option("max-filename-length",{default:void 0,defaultDescription:"60",describe:"If `manage-filenames` is enabled, this option specifies the maximum length of the filename in characters.",type:"number"}).option(k("Output the sync report as JSON.")).option(v),async({ankiAutoLaunch:e,ankiConnect:t,ankiWeb:i,directory:o,dryRun:a,json:r,manageFilenames:n,maxFilenameLength:p,namespace:f,recursive:c=!0,verbose:u})=>{d.verbose=u;const l=Q(o),E=c?`${l}/**/*.md`:`${l}/*.md`,C=await H(E,{absolute:!0});if(C.length===0){d.error(`No Markdown files found in "${l}".`),process.exitCode=1;return}n==="off"&&p!==void 0&&d.warn("Ignoring `max-filename-length` option because `manage-filenames` is not enabled.");const{host:$,port:P}=h(t),O=await T(C,{ankiConnectOptions:{autoLaunch:e,host:$,port:P},ankiWeb:i,dryRun:a,manageFilenames:n,maxFilenameLength:p,namespace:f}).catch(b);r?(process.stdout.write(JSON.stringify(O,void 0,2)),process.stdout.write(`
3
3
  `)):(process.stderr.write(D(O,u)),process.stderr.write(`
4
- `))}).command("list [options]","Utility command to list Yanki-created notes in the Anki database.",e=>e.option(A("Advanced option to list notes in a specific namespace. Case insensitive. Notes from the default internal namespace are listed by default. Pass `'*'` to list all Yanki-created notes in the Anki database.")).options(w).options(y).option(g("Output the list of notes as JSON to stdout.")),async({ankiAutoLaunch:e,ankiConnect:t,json:i,namespace:o})=>{const{host:a,port:r}=h(t),n=await I({ankiConnectOptions:{autoLaunch:e,host:a,port:r},namespace:o}).catch(k);i?(process.stdout.write(JSON.stringify(n,void 0,2)),process.stdout.write(`
4
+ `))}).command("list [options]","Utility command to list Yanki-created notes in the Anki database.",e=>e.option(A("Advanced option to list notes in a specific namespace. Case insensitive. Notes from the default internal namespace are listed by default. Pass `'*'` to list all Yanki-created notes in the Anki database.")).options(w).options(y).option(k("Output the list of notes as JSON to stdout.")),async({ankiAutoLaunch:e,ankiConnect:t,json:i,namespace:o})=>{const{host:a,port:r}=h(t),n=await I({ankiConnectOptions:{autoLaunch:e,host:a,port:r},namespace:o}).catch(b);i?(process.stdout.write(JSON.stringify(n,void 0,2)),process.stdout.write(`
5
5
  `)):(process.stdout.write(U(n)),process.stdout.write(`
6
- `))}).command("delete [options]","Utility command to manually delete Yanki-created notes in the Anki database. This is for advanced use cases, usually the `sync` command takes care of deleting files from Anki Database once they're removed from the local file system.",e=>e.option(S).option(A("Advanced option to list notes in a specific namespace. Case insensitive. Notes from the default internal namespace are listed by default. If you've synced notes to multiple namespaces, Pass `'*'` to delete all Yanki-created notes in the Anki database.")).options(w).options(y).option(b).option(g("Output the list of deleted notes as JSON to stdout.")).option(v),async({ankiAutoLaunch:e,ankiConnect:t,ankiWeb:i,dryRun:o,json:a,namespace:r,verbose:n})=>{const{host:p,port:f}=h(t),c=await L({ankiConnectOptions:{autoLaunch:e,host:p,port:f},ankiWeb:i,dryRun:o,namespace:r}).catch(k);a?(process.stdout.write(JSON.stringify(c,void 0,2)),process.stdout.write(`
6
+ `))}).command("delete [options]","Utility command to manually delete Yanki-created notes in the Anki database. This is for advanced use cases, usually the `sync` command takes care of deleting files from Anki Database once they're removed from the local file system.",e=>e.option(S).option(A("Advanced option to list notes in a specific namespace. Case insensitive. Notes from the default internal namespace are listed by default. If you've synced notes to multiple namespaces, Pass `'*'` to delete all Yanki-created notes in the Anki database.")).options(w).options(y).option(g).option(k("Output the list of deleted notes as JSON to stdout.")).option(v),async({ankiAutoLaunch:e,ankiConnect:t,ankiWeb:i,dryRun:o,json:a,namespace:r,verbose:n})=>{const{host:p,port:f}=h(t),c=await L({ankiConnectOptions:{autoLaunch:e,host:p,port:f},ankiWeb:i,dryRun:o,namespace:r}).catch(b);a?(process.stdout.write(JSON.stringify(c,void 0,2)),process.stdout.write(`
7
7
  `)):(process.stderr.write(M(c,n)),process.stderr.write(`
8
- `))}).command("style [options]","Utility command to set the CSS stylesheet for all present and future Yanki-created notes.",e=>e.option(S).option("css",{alias:"c",default:void 0,describe:"Path to the CSS stylesheet to set for all Yanki-created notes. If not provided, the default Anki stylesheet is used.",type:"string"}).options(w).options(y).option(b).option(g("Output the list of updated note types / models as JSON to stdout.")).option(v),async({ankiAutoLaunch:e,ankiConnect:t,ankiWeb:i,css:o,dryRun:a,json:r,verbose:n})=>{const{host:p,port:f}=h(t);let c;if(o!==void 0){if(W.extname(o)!==".css"){d.error("The provided CSS file must have a .css extension."),process.exitCode=1;return}try{c=await F.readFile(o,"utf8")}catch(l){l instanceof Error?d.error(`Error loading CSS file: ${l.message}`):d.error(`Unknown error loading CSS file: ${String(l)}`),process.exitCode=1;return}}const u=await z({ankiConnectOptions:{autoLaunch:e,host:p,port:f},ankiWeb:i,css:c??void 0,dryRun:a}).catch(k);r?(process.stdout.write(JSON.stringify(u,void 0,2)),process.stdout.write(`
9
- `)):(process.stderr.write(H(u,n)),process.stderr.write(`
8
+ `))}).command("style [options]","Utility command to set the CSS stylesheet for all present and future Yanki-created notes.",e=>e.option(S).option("css",{alias:"c",default:void 0,describe:"Path to the CSS stylesheet to set for all Yanki-created notes. If not provided, the default Anki stylesheet is used.",type:"string"}).options(w).options(y).option(g).option(k("Output the list of updated note types / models as JSON to stdout.")).option(v),async({ankiAutoLaunch:e,ankiConnect:t,ankiWeb:i,css:o,dryRun:a,json:r,verbose:n})=>{const{host:p,port:f}=h(t);let c;if(o!==void 0){if(W.extname(o)!==".css"){d.error("The provided CSS file must have a .css extension."),process.exitCode=1;return}try{c=await F.readFile(o,"utf8")}catch(l){l instanceof Error?d.error(`Error loading CSS file: ${l.message}`):d.error(`Unknown error loading CSS file: ${String(l)}`),process.exitCode=1;return}}const u=await z({ankiConnectOptions:{autoLaunch:e,host:p,port:f},ankiWeb:i,css:c??void 0,dryRun:a}).catch(b);r?(process.stdout.write(JSON.stringify(u,void 0,2)),process.stdout.write(`
9
+ `)):(process.stderr.write(j(u,n)),process.stderr.write(`
10
10
  `))}).demandCommand(1).alias("h","help").version(K).alias("v","version").help().wrap(process.stdout.isTTY?Math.min(120,R.terminalWidth()):0).parse();
@@ -884,13 +884,41 @@ type ListReport = {
884
884
  notes: YankiNote[];
885
885
  };
886
886
  /**
887
- * Description List notes...
887
+ * Description List notes currently in Anki...
888
888
  * @param options
889
889
  * @returns
890
890
  */
891
891
  declare function listNotes(options?: PartialDeep<ListOptions>): Promise<ListReport>;
892
892
  declare function formatListReport(report: ListReport): string;
893
893
 
894
+ type RenameFilesReport = {
895
+ dryRun: boolean;
896
+ notes: Array<{
897
+ filePath: string;
898
+ filePathOriginal: string;
899
+ markdown: string;
900
+ note: YankiNote;
901
+ }>;
902
+ };
903
+ type RenameFilesOptions = {
904
+ dryRun: boolean;
905
+ manageFilenames: 'off' | 'prompt' | 'response';
906
+ maxFilenameLength: number;
907
+ namespace: string;
908
+ obsidianVault: string | undefined;
909
+ };
910
+ declare const defaultRenameFilesOptions: RenameFilesOptions;
911
+ /**
912
+ * Also loads the notes from markdown and sets deck names...
913
+ * @param allLocalFilePaths
914
+ * @param options
915
+ * @param readFile
916
+ * @param writeFile
917
+ * @param rename
918
+ */
919
+ declare function renameFiles(allLocalFilePaths: string[], options: Partial<RenameFilesOptions>, readFile?: (filePath: string) => Promise<string>, writeFile?: (filePath: string, data: string) => Promise<void>, // Not used, yet
920
+ rename?: (oldPath: string, newPath: string) => Promise<void>): Promise<RenameFilesReport>;
921
+
894
922
  type StyleOptions = {
895
923
  ankiConnectOptions: YankiConnectOptions;
896
924
  /**
@@ -971,34 +999,6 @@ declare function syncNotes(allLocalNotes: YankiNote[], options?: PartialDeep<Syn
971
999
  * about the sync @throws
972
1000
  */
973
1001
  declare function syncFiles(allLocalFilePaths: string[], options?: PartialDeep<SyncOptions>, readFile?: (filePath: string) => Promise<string>, writeFile?: (filePath: string, data: string) => Promise<void>, rename?: (oldPath: string, newPath: string) => Promise<void>): Promise<SyncReport>;
974
- /**
975
- * Helper function to infer deck names from file paths if `deckName` not defined in the note's frontmatter.
976
- *
977
- * `deckName` will always override the inferred deck name.
978
- *
979
- * Depends on the context of _all_ file paths passed to `syncNoteFiles`.
980
- *
981
- * Example of paths -> deck names with `common-root`:
982
- * /base/foo/note.md -> foo
983
- * /base/foo/baz/note.md -> foo::baz
984
- *
985
- * Example of paths -> deck names with `common-root`:
986
- * /base/foo/note.md -> foo
987
- * /base/foo/note.md -> foo
988
- *
989
- * Example of paths -> deck names with `common-parent`:
990
- * /base/foo/note.md -> base::foo
991
- * /base/foo/baz/note.md -> base::foo::baz
992
- *
993
- * Example of paths -> deck names with `common-parent`:
994
- * /base/foo/note.md -> foo
995
- * /base/foo/note.md -> foo
996
- *
997
- * @param absoluteFilePaths Absolute paths to all markdown Anki note files. (Ensures proper resolution if path module is polyfilled.)
998
- * @param prune If true, deck names are not allowed to "jump" over empty directories, even if there are other note files somewhere up the hierarchy
999
- * @returns array of ::-delimited deck paths
1000
- */
1001
- declare function getDeckNamesFromFilePaths(absoluteFilePaths: string[], mode?: 'common-parent' | 'common-root'): string[];
1002
1002
  declare function formatSyncReport(report: SyncReport, verbose?: boolean): string;
1003
1003
 
1004
1004
  /**
@@ -1024,4 +1024,4 @@ declare function urlToHostAndPort(url: string): {
1024
1024
  };
1025
1025
  declare function hostAndPortToUrl(host: string, port: number): string;
1026
1026
 
1027
- export { type CleanOptions, type CleanReport, type ListOptions, type StyleOptions, type StyleReport, type SyncOptions, type SyncReport, type YankiNote, cleanNotes, defaultCleanOptions, defaultListOptions, defaultStyleOptions, defaultSyncOptions, formatCleanReport, formatListReport, formatStyleReport, formatSyncReport, getDeckNamesFromFilePaths, getNoteFromMarkdown, hostAndPortToUrl, listNotes, setStyle, syncFiles, syncNotes, urlToHostAndPort };
1027
+ export { type CleanOptions, type CleanReport, type ListOptions, type RenameFilesOptions, type RenameFilesReport, type StyleOptions, type StyleReport, type SyncOptions, type SyncReport, type YankiNote, cleanNotes, defaultCleanOptions, defaultListOptions, defaultRenameFilesOptions, defaultStyleOptions, defaultSyncOptions, formatCleanReport, formatListReport, formatStyleReport, formatSyncReport, getNoteFromMarkdown, hostAndPortToUrl, listNotes, renameFiles, setStyle, syncFiles, syncNotes, urlToHostAndPort };
package/dist/lib/index.js CHANGED
@@ -1 +1 @@
1
- import{c as l,d as n,a as i,e as m,h as p,f,b as d,g as c,i as y,j as u,n as N,o as S,l as F,s as O,k as R,m as g,u as h}from"../sync-CNwtrBlB.js";import"rehype-mathjax";import"rehype-parse";import"node:path";import"node:crypto";export{l as cleanNotes,n as defaultCleanOptions,i as defaultListOptions,m as defaultStyleOptions,p as defaultSyncOptions,f as formatCleanReport,d as formatListReport,c as formatStyleReport,y as formatSyncReport,u as getDeckNamesFromFilePaths,N as getNoteFromMarkdown,S as hostAndPortToUrl,F as listNotes,O as setStyle,R as syncFiles,g as syncNotes,h as urlToHostAndPort};
1
+ import{c as l,d as n,a as i,e as p,g as m,i as f,f as d,b as u,h as y,j as c,n as O,o as R,l as S,r as F,s as N,k as g,m as h,u as k}from"../sync-CnK6Mxek.js";import"rehype-mathjax";import"rehype-parse";import"node:path";import"node:crypto";export{l as cleanNotes,n as defaultCleanOptions,i as defaultListOptions,p as defaultRenameFilesOptions,m as defaultStyleOptions,f as defaultSyncOptions,d as formatCleanReport,u as formatListReport,y as formatStyleReport,c as formatSyncReport,O as getNoteFromMarkdown,R as hostAndPortToUrl,S as listNotes,F as renameFiles,N as setStyle,g as syncFiles,h as syncNotes,k as urlToHostAndPort};