yanki 0.18.12 → 0.18.13

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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var z=Object.defineProperty;var p=(e,t)=>z(e,"name",{value:t,configurable:!0});import{globby as E}from"globby";import D from"node:fs/promises";import B from"node:path";import H from"node:os";import W from"yargs";import{hideBin as j}from"yargs/helpers";import{w as f,v as q,x as S,n as G,m as V,l as K,b as Q,c as X,f as Z,s as _,i as ee}from"../sync-files-BzQaOHFQ.js";import s from"chalk";import"process";import"rehype-parse";import"node:crypto";const M=H.homedir();function te(e){if(typeof e!="string")throw new TypeError(`Expected a string, got ${typeof e}`);return M?e.replace(/^~(?=$|\/|\\)/,M):e}p(te,"untildify");var oe="0.18.12";const I=process?.versions?.node!==void 0,c={verbose:!1,log(...e){if(!this.verbose)return;const t=s.gray("[Log]");I?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]");I?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"}},C={"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"}},O={verbose:{default:!1,describe:"Enable verbose logging.",type:"boolean"}};function k(e){return{json:{default:!1,describe:e,type:"boolean"}}}p(k,"jsonOption");const x={"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 R(e){return{namespace:{alias:"n",default:f.namespace,describe:e,type:"string"}}}p(R,"namespaceOption");const ne={"strict-line-breaks":{alias:"b",default:f.strictLineBreaks,describe:"Set to false to treat single newlines in Markdown as line breaks.",type:"boolean"}};function b(e){const t=q(e);if(t===void 0)throw new Error(`Invalid AnkiConnect URL: "${e}"`);return t}p(b,"urlToHostAndPortValidated");const g=p(e=>{throw e instanceof Error?(e.cause?.code==="ECONNREFUSED"&&(c.error("Failed to connect to Anki. Make sure Anki is running and AnkiConnect is installed."),process.exitCode=1,process.exit()),e):new Error("Unknown error")},"ankiNotRunningErrorHandler"),L=W(j(process.argv));await L.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(x).option(R("Advanced option for managing multiple Yanki synchronization groups. Case insensitive. See the readme for more information.")).option(w).option(y).option(C).option("manage-filenames",{alias:"m",choices:["off","prompt","response"],default:f.manageFilenames,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:String(f.maxFilenameLength),describe:"If `manage-filenames` is enabled, this option specifies the maximum length of the filename in characters.",type:"number"}).option("sync-media",{alias:"s",choices:["off","all","local","remote"],default:f.syncMediaAssets,describe:"Sync image, video, and audio assets to Anki's media storage system. Clean up is managed automatically. The `all` argument will save both local and remote assets to Anki, while `local` will only save local assets, `remote` will only save remote assets, and `off` will not save any assets.",type:"string"}).option("strict-matching",{default:f.strictMatching,describe:'Consider notes to be a "match" only if the local Markdown frontmatter `noteId` matches the remote Anki database `noteId` exactly. When disabled, Yanki will attempt to reuse existing Anki notes whose content matches a local Markdown note, even if the local and remote `noteId` differs. This helps preserve study progress in Anki if the local Markdown frontmatter is lost or corrupted. In Yanki 0.17.0 and earlier, `--strict-matching` was the default behavior. Starting with version 0.18.0, it is disabled by default and may be enabled via this flag.',type:"boolean"}).option(ne).option(k("Output the sync report as JSON.")).option(O),async({ankiAutoLaunch:e,ankiConnect:t,ankiWeb:a,directory:o,dryRun:i,json:r,manageFilenames:n,maxFilenameLength:d,namespace:u,recursive:l=!0,strictLineBreaks:h,strictMatching:m,syncMedia:Y,verbose:P})=>{c.verbose=P;const v=S(te(o)),F=l?`${v}/**/*.md`:`${v}/*.md`,N=(await E(F,{absolute:!0})).map(A=>S(A)),T=(await E(`${v}/**/*`,{absolute:!0})).map(A=>S(A));if(N.length===0){c.error(`No Markdown files found in "${o}".`),process.exitCode=1;return}n==="off"&&d!==void 0&&c.warn("Ignoring `max-filename-length` option because `manage-filenames` is not enabled.");const{host:U,port:J}=b(t),$=await G(N,{allFilePaths:T,ankiConnectOptions:{autoLaunch:e,host:U,port:J},ankiWeb:a,dryRun:i,manageFilenames:n,maxFilenameLength:d,namespace:u,strictLineBreaks:h,strictMatching:m,syncMediaAssets:Y}).catch(g);r?(process.stdout.write(JSON.stringify($,void 0,2)),process.stdout.write(`
2
+ var z=Object.defineProperty;var p=(e,t)=>z(e,"name",{value:t,configurable:!0});import{globby as E}from"globby";import D from"node:fs/promises";import B from"node:path";import H from"node:os";import W from"yargs";import{hideBin as j}from"yargs/helpers";import{w as f,v as q,x as S,n as G,m as V,l as K,b as Q,c as X,f as Z,s as _,i as ee}from"../sync-files-pI8q7ieP.js";import s from"chalk";import"process";import"rehype-parse";import"node:crypto";const M=H.homedir();function te(e){if(typeof e!="string")throw new TypeError(`Expected a string, got ${typeof e}`);return M?e.replace(/^~(?=$|\/|\\)/,M):e}p(te,"untildify");var oe="0.18.13";const I=process?.versions?.node!==void 0,c={verbose:!1,log(...e){if(!this.verbose)return;const t=s.gray("[Log]");I?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]");I?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"}},C={"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"}},O={verbose:{default:!1,describe:"Enable verbose logging.",type:"boolean"}};function k(e){return{json:{default:!1,describe:e,type:"boolean"}}}p(k,"jsonOption");const x={"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 R(e){return{namespace:{alias:"n",default:f.namespace,describe:e,type:"string"}}}p(R,"namespaceOption");const ne={"strict-line-breaks":{alias:"b",default:f.strictLineBreaks,describe:"Set to false to treat single newlines in Markdown as line breaks.",type:"boolean"}};function b(e){const t=q(e);if(t===void 0)throw new Error(`Invalid AnkiConnect URL: "${e}"`);return t}p(b,"urlToHostAndPortValidated");const g=p(e=>{throw e instanceof Error?(e.cause?.code==="ECONNREFUSED"&&(c.error("Failed to connect to Anki. Make sure Anki is running and AnkiConnect is installed."),process.exitCode=1,process.exit()),e):new Error("Unknown error")},"ankiNotRunningErrorHandler"),L=W(j(process.argv));await L.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(x).option(R("Advanced option for managing multiple Yanki synchronization groups. Case insensitive. See the readme for more information.")).option(w).option(y).option(C).option("manage-filenames",{alias:"m",choices:["off","prompt","response"],default:f.manageFilenames,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:String(f.maxFilenameLength),describe:"If `manage-filenames` is enabled, this option specifies the maximum length of the filename in characters.",type:"number"}).option("sync-media",{alias:"s",choices:["off","all","local","remote"],default:f.syncMediaAssets,describe:"Sync image, video, and audio assets to Anki's media storage system. Clean up is managed automatically. The `all` argument will save both local and remote assets to Anki, while `local` will only save local assets, `remote` will only save remote assets, and `off` will not save any assets.",type:"string"}).option("strict-matching",{default:f.strictMatching,describe:'Consider notes to be a "match" only if the local Markdown frontmatter `noteId` matches the remote Anki database `noteId` exactly. When disabled, Yanki will attempt to reuse existing Anki notes whose content matches a local Markdown note, even if the local and remote `noteId` differs. This helps preserve study progress in Anki if the local Markdown frontmatter is lost or corrupted. In Yanki 0.17.0 and earlier, `--strict-matching` was the default behavior. Starting with version 0.18.0, it is disabled by default and may be enabled via this flag.',type:"boolean"}).option(ne).option(k("Output the sync report as JSON.")).option(O),async({ankiAutoLaunch:e,ankiConnect:t,ankiWeb:a,directory:o,dryRun:i,json:r,manageFilenames:n,maxFilenameLength:d,namespace:u,recursive:l=!0,strictLineBreaks:h,strictMatching:m,syncMedia:Y,verbose:P})=>{c.verbose=P;const v=S(te(o)),F=l?`${v}/**/*.md`:`${v}/*.md`,N=(await E(F,{absolute:!0})).map(A=>S(A)),T=(await E(`${v}/**/*`,{absolute:!0})).map(A=>S(A));if(N.length===0){c.error(`No Markdown files found in "${o}".`),process.exitCode=1;return}n==="off"&&d!==void 0&&c.warn("Ignoring `max-filename-length` option because `manage-filenames` is not enabled.");const{host:U,port:J}=b(t),$=await G(N,{allFilePaths:T,ankiConnectOptions:{autoLaunch:e,host:U,port:J},ankiWeb:a,dryRun:i,manageFilenames:n,maxFilenameLength:d,namespace:u,strictLineBreaks:h,strictMatching:m,syncMediaAssets:Y}).catch(g);r?(process.stdout.write(JSON.stringify($,void 0,2)),process.stdout.write(`
3
3
  `)):(process.stderr.write(V($,P)),process.stderr.write(`
4
4
  `))}).command("list [options]","Utility command to list Yanki-created notes in the Anki database.",e=>e.option(R("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:a,namespace:o})=>{const{host:i,port:r}=b(t),n=await K({ankiConnectOptions:{autoLaunch:e,host:i,port:r},namespace:o}).catch(g);a?(process.stdout.write(JSON.stringify(n,void 0,2)),process.stdout.write(`
5
5
  `)):(process.stdout.write(Q(n)),process.stdout.write(`
@@ -632,27 +632,29 @@ const partialSettings: PartialDeep<Settings, {recurseIntoArrays: true}> = {
632
632
  type PartialDeep<T, Options extends PartialDeepOptions = {}> =
633
633
  _PartialDeep<T, ApplyDefaultOptions<PartialDeepOptions, DefaultPartialDeepOptions, Options>>;
634
634
 
635
- type _PartialDeep<T, Options extends Required<PartialDeepOptions>> = T extends BuiltIns | (((...arguments_: any[]) => unknown)) | (new (...arguments_: any[]) => unknown)
635
+ type _PartialDeep<T, Options extends Required<PartialDeepOptions>> = T extends BuiltIns | ((new (...arguments_: any[]) => unknown))
636
636
  ? T
637
- : T extends Map<infer KeyType, infer ValueType>
638
- ? PartialMapDeep<KeyType, ValueType, Options>
639
- : T extends Set<infer ItemType>
640
- ? PartialSetDeep<ItemType, Options>
641
- : T extends ReadonlyMap<infer KeyType, infer ValueType>
642
- ? PartialReadonlyMapDeep<KeyType, ValueType, Options>
643
- : T extends ReadonlySet<infer ItemType>
644
- ? PartialReadonlySetDeep<ItemType, Options>
645
- : T extends object
646
- ? T extends ReadonlyArray<infer ItemType> // Test for arrays/tuples, per https://github.com/microsoft/TypeScript/issues/35156
647
- ? Options['recurseIntoArrays'] extends true
648
- ? ItemType[] extends T // Test for arrays (non-tuples) specifically
649
- ? readonly ItemType[] extends T // Differentiate readonly and mutable arrays
650
- ? ReadonlyArray<_PartialDeep<Options['allowUndefinedInNonTupleArrays'] extends false ? ItemType : ItemType | undefined, Options>>
651
- : Array<_PartialDeep<Options['allowUndefinedInNonTupleArrays'] extends false ? ItemType : ItemType | undefined, Options>>
652
- : PartialObjectDeep<T, Options> // Tuples behave properly
653
- : T // If they don't opt into array testing, just use the original type
654
- : PartialObjectDeep<T, Options>
655
- : unknown;
637
+ : IsNever<keyof T> extends true // For functions with no properties
638
+ ? T
639
+ : T extends Map<infer KeyType, infer ValueType>
640
+ ? PartialMapDeep<KeyType, ValueType, Options>
641
+ : T extends Set<infer ItemType>
642
+ ? PartialSetDeep<ItemType, Options>
643
+ : T extends ReadonlyMap<infer KeyType, infer ValueType>
644
+ ? PartialReadonlyMapDeep<KeyType, ValueType, Options>
645
+ : T extends ReadonlySet<infer ItemType>
646
+ ? PartialReadonlySetDeep<ItemType, Options>
647
+ : T extends object
648
+ ? T extends ReadonlyArray<infer ItemType> // Test for arrays/tuples, per https://github.com/microsoft/TypeScript/issues/35156
649
+ ? Options['recurseIntoArrays'] extends true
650
+ ? ItemType[] extends T // Test for arrays (non-tuples) specifically
651
+ ? readonly ItemType[] extends T // Differentiate readonly and mutable arrays
652
+ ? ReadonlyArray<_PartialDeep<Options['allowUndefinedInNonTupleArrays'] extends false ? ItemType : ItemType | undefined, Options>>
653
+ : Array<_PartialDeep<Options['allowUndefinedInNonTupleArrays'] extends false ? ItemType : ItemType | undefined, Options>>
654
+ : PartialObjectDeep<T, Options> // Tuples behave properly
655
+ : T // If they don't opt into array testing, just use the original type
656
+ : PartialObjectDeep<T, Options>
657
+ : unknown;
656
658
 
657
659
  /**
658
660
  Same as `PartialDeep`, but accepts only `Map`s and as inputs. Internal helper for `PartialDeep`.
@@ -677,9 +679,12 @@ type PartialReadonlySetDeep<T, Options extends Required<PartialDeepOptions>> = {
677
679
  /**
678
680
  Same as `PartialDeep`, but accepts only `object`s as inputs. Internal helper for `PartialDeep`.
679
681
  */
680
- type PartialObjectDeep<ObjectType extends object, Options extends Required<PartialDeepOptions>> = {
681
- [KeyType in keyof ObjectType]?: _PartialDeep<ObjectType[KeyType], Options>
682
- };
682
+ type PartialObjectDeep<ObjectType extends object, Options extends Required<PartialDeepOptions>> =
683
+ (ObjectType extends (...arguments_: any) => unknown
684
+ ? (...arguments_: Parameters<ObjectType>) => ReturnType<ObjectType>
685
+ : {}) & ({
686
+ [KeyType in keyof ObjectType]?: _PartialDeep<ObjectType[KeyType], Options>
687
+ });
683
688
 
684
689
  type CardBrowserColumns = 'answer' | 'cardDue' | 'cardEase' | 'cardIvl' | 'cardLapses' | 'cardMod' | 'cardReps' | 'deck' | 'note' | 'noteCrt' | 'noteFld' | 'noteMod' | 'noteTags' | 'question' | 'template' | (string & {});
685
690
  type CardValueKeys = 'data' | 'did' | 'due' | 'factor' | 'flags' | 'id' | 'ivl' | 'lapses' | 'left' | 'mod' | 'odid' | 'odue' | 'ord' | 'queue' | 'reps' | 'type' | 'usn';
package/dist/lib/index.js CHANGED
@@ -1 +1 @@
1
- import{c as n,d as i,q as r,g as f,a as m,e as p,h as u,k as d,o as y,f as S,b as O,i as c,m as F,t as N,j as R,u as g,l as k,r as h,s as w,n as A,p as C,v as G}from"../sync-files-BzQaOHFQ.js";import"process";import"rehype-parse";import"node:path";import"node:crypto";export{n as cleanNotes,i as defaultCleanOptions,r as defaultGetNoteFromMarkdownOptions,f as defaultGetStyleOptions,m as defaultListOptions,p as defaultRenameFilesOptions,u as defaultSetStyleOptions,d as defaultSyncFilesOptions,y as defaultSyncNotesOptions,S as formatCleanResult,O as formatListResult,c as formatSetStyleResult,F as formatSyncFilesResult,N as getNoteFromMarkdown,R as getStyle,g as hostAndPortToUrl,k as listNotes,h as renameFiles,w as setStyle,A as syncFiles,C as syncNotes,G as urlToHostAndPort};
1
+ import{c as n,d as i,q as r,g as f,a as m,e as p,h as u,k as d,o as y,f as S,b as O,i as c,m as F,t as N,j as R,u as g,l as k,r as h,s as w,n as A,p as C,v as G}from"../sync-files-pI8q7ieP.js";import"process";import"rehype-parse";import"node:path";import"node:crypto";export{n as cleanNotes,i as defaultCleanOptions,r as defaultGetNoteFromMarkdownOptions,f as defaultGetStyleOptions,m as defaultListOptions,p as defaultRenameFilesOptions,u as defaultSetStyleOptions,d as defaultSyncFilesOptions,y as defaultSyncNotesOptions,S as formatCleanResult,O as formatListResult,c as formatSetStyleResult,F as formatSyncFilesResult,N as getNoteFromMarkdown,R as getStyle,g as hostAndPortToUrl,k as listNotes,h as renameFiles,w as setStyle,A as syncFiles,C as syncNotes,G as urlToHostAndPort};