Compare commits

..

1 Commits

  1. 3
      .gitignore
  2. 5
      .gulp.json
  3. 6
      gulpfile.cjs
  4. 96
      journal.yaml
  5. 4162
      package-lock.json
  6. 24
      package.json
  7. 52
      src/commands/AddEntry.ts
  8. 22
      src/commands/UpdateEmpathyGuide.ts
  9. 13
      src/datatypes/Condition.ts
  10. 2
      src/datatypes/EmpathyGroup.ts
  11. 2
      src/datatypes/EmpathyGroupList.ts
  12. 4
      src/datatypes/EmpathyGuide.ts
  13. 56
      src/datatypes/Entry.ts
  14. 33
      src/datatypes/GuidedEmpathy.ts
  15. 4
      src/datatypes/Journal.ts
  16. 2
      src/datatypes/Persona.ts
  17. 96
      src/datatypes/SleepRecord.ts
  18. 15
      src/datatypes/Suicidality.ts
  19. 12
      src/index.ts
  20. 10
      src/prompts/Inquire.ts
  21. 16
      src/prompts/implementations/ConditionPrompt.ts
  22. 11
      src/prompts/implementations/EmpathyGroupListPrompt.ts
  23. 17
      src/prompts/implementations/EmpathyGroupPrompt.ts
  24. 26
      src/prompts/implementations/EmpathyGuidePrompt.ts
  25. 31
      src/prompts/implementations/EntryMainMenuPrompt.ts
  26. 10
      src/prompts/implementations/EntryPrompt.ts
  27. 18
      src/prompts/implementations/GuidedEmpathyListPrompt.ts
  28. 18
      src/prompts/implementations/GuidedEmpathyPrompt.ts
  29. 10
      src/prompts/implementations/JournalEntryPrompt.ts
  30. 196
      src/prompts/implementations/SleepRecordPrompt.ts
  31. 10
      src/prompts/implementations/SuicidalityPrompt.ts
  32. 10
      src/prompts/implementations/SummaryPrompt.ts
  33. 386
      src/prompts/types/DateInput.ts
  34. 50
      src/prompts/types/HierarchicalCheckboxInput.ts
  35. 14
      src/prompts/types/MultiTextInput.ts
  36. 8
      src/prompts/types/index.ts
  37. 29
      src/repository/LocalRepository.ts
  38. 49
      src/schemata/SchemaData.ts
  39. 236
      src/schemata/Serialization.ts
  40. 46
      src/schemata/YAMLPrompt.ts
  41. 2
      src/utils/Merge.ts
  42. 2
      tsconfig.json

3
.gitignore vendored

@ -38,5 +38,4 @@ Thumbs.db
dist/**/*
# ignore yarn.lock
yarn.lock
/release
yarn.lock

@ -1,5 +0,0 @@
{
"flags": {
"gulpfile": "gulpfile.cjs"
}
}

@ -1,6 +0,0 @@
const gulp = require("gulp");
const ts = require("gulp-typescript");
const tsProject = ts.createProject("tsconfig.json");
gulp.task("default", function () {
return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist"));
});

@ -0,0 +1,96 @@
empathyGuide:
feelings:
pleasant:
- header: Affectionate
items:
- compassionate
- fond
- loving
- openhearted
- tender
- warm
- header: Engaged
items:
- absorbed
- curious
- enchanted
- engaged
- engrossed
- enthralled
- entranced
- fascinated
- interested
- intrigued
- involved
- open
- spellbound
- stimulated
- header: Excited
items:
- amazed
- ardent
- aroused
- dazzled
- energetic
- enlivened
- enthusiastic
- exuberant
- invigorated
- lively
- passionate
- surprised
- vibrant
unpleasant: []
needs:
- header: Autonomy
items:
- autonomy
- choice
- dignity
- freedom
- independence
- self-expression
- space
- spontaneity
- header: Connection
items:
- acceptance
- affection
- appreciation
- authenticity
- belonging
- care
- closeness
- communication
- communion
- community
- companionship
- compassion
- connection
- consideration
- empathy
- friendship
- inclusion
- inspiration
- integrity
- intimacy
- love
- mutuality
- nurturing
- partnership
- presence
- respect/self-respect
- security
- self-acceptance
- self-care
- self-connection
- self-expression
- shared reality
- stability
- support
- to know and be known
- to see and be seen
- trust
- understanding
- warmth
entries: [ ]

4162
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -4,29 +4,30 @@
"description": "The guided journaling software used to track Mari's status.",
"main": "index.js",
"scripts": {
"start": "npm run --silent app",
"start": "npm run app",
"app": "node dist/index.js",
"update-empathy-guide": "npm run --silent app -- update-empathy-guide",
"add-entry": "npm run --silent app -- add-entry",
"build": "gulp",
"prestart": "npm run --silent build",
"debug": "npm run --silent start --inspect"
"update-empathy-guide": "npm run app -- update-empathy-guide",
"add-entry": "npm run app -- add-entry",
"build": "npm run build-ts",
"prestart": "npm run build",
"debug": "npm run start --inspect",
"watch-node": "nodemon dist/index.js",
"watch": "tsc && concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"",
"build-ts": "tsc",
"watch-ts": "tsc -w"
},
"type": "module",
"keywords": [],
"author": "Mari",
"author": "",
"license": "ISC",
"dependencies": {
"ajv": "^8.6.2",
"capitalize": "^2.0.3",
"chalk": "^2.4.2",
"cli-cursor": "^4.0.0",
"env-paths": "^2.2.1",
"figures": "^3.2.0",
"fuzzy": "^0.1.3",
"inquirer": "^8.1.2",
"js-yaml": "^4.1.0",
"luxon": "^2.0.2",
"make-dir": "^2.1.0",
"pluralize": "^8.0.0",
"rxjs": "^6.6.7",
@ -41,13 +42,10 @@
"@types/concurrently": "^5.2.1",
"@types/inquirer": "^7.3.3",
"@types/js-yaml": "^4.0.2",
"@types/luxon": "^2.0.4",
"@types/node-fetch": "^2.5.4",
"@types/pluralize": "^0.0.29",
"@types/yargs": "^17.0.0",
"concurrently": "^5.0.0",
"gulp": "^4.0.2",
"gulp-typescript": "^6.0.0-alpha.1",
"nodemon": "^2.0.4",
"typescript": "^4.0.3"
}

@ -1,18 +1,16 @@
import {Separator} from "../prompts/Inquire.js";
import {inquire} from "../prompts/Inquire.js";
import {summaryPrompt} from "../prompts/implementations/SummaryPrompt.js";
import {journalEntryPrompt} from "../prompts/implementations/JournalEntryPrompt.js";
import {guidedEmpathyListPrompt} from "../prompts/implementations/GuidedEmpathyListPrompt.js";
import {entryMainMenuPrompt} from "../prompts/implementations/EntryMainMenuPrompt.js";
import {Separator} from "inquirer";
import {inquire} from "../prompts/Inquire";
import {summaryPrompt} from "../prompts/implementations/SummaryPrompt";
import {journalEntryPrompt} from "../prompts/implementations/JournalEntryPrompt";
import {guidedEmpathyListPrompt} from "../prompts/implementations/GuidedEmpathyListPrompt";
import {entryMainMenuPrompt} from "../prompts/implementations/EntryMainMenuPrompt";
import {CommandModule} from "yargs";
import { registerPrompts } from "../prompts/types/index.js";
import {LocalRepository} from "../repository/LocalRepository.js";
import {conditionPrompt} from "../prompts/implementations/ConditionPrompt.js";
import {entryPrompt} from "../prompts/implementations/EntryPrompt.js";
import {guidedEmpathyPrompt} from "../prompts/implementations/GuidedEmpathyPrompt.js";
import {suicidalityPrompt} from "../prompts/implementations/SuicidalityPrompt.js";
import {yamlPrompt} from "../schemata/YAMLPrompt.js";
import {sleepRecordPrompt} from "../prompts/implementations/SleepRecordPrompt.js";
import { registerPrompts } from "../prompts/types";
import {LocalRepository} from "../repository/LocalRepository";
import {conditionPrompt} from "../prompts/implementations/ConditionPrompt";
import {entryPrompt} from "../prompts/implementations/EntryPrompt";
import {guidedEmpathyPrompt} from "../prompts/implementations/GuidedEmpathyPrompt";
import {suicidalityPrompt} from "../prompts/implementations/SuicidalityPrompt";
export function addEntryCommand(): CommandModule {
return {
@ -27,7 +25,6 @@ export function addEntryCommand(): CommandModule {
const summary = summaryPrompt({inquire})
const journal = journalEntryPrompt({inquire})
const suicidality = suicidalityPrompt({inquire})
const sleep = sleepRecordPrompt({inquire})
const empathy = guidedEmpathyPrompt({
inquire,
guideFactory: () => empathyGuide,
@ -36,47 +33,30 @@ export function addEntryCommand(): CommandModule {
const empathyList = guidedEmpathyListPrompt({inquire, promptForEmpathy: empathy})
const mainMenuItems = [
// TODO: add ability to amend previous entry
// TODO: add To-Do list items, which can be in four states (Pending, Done, Suspended, Canceled)
// A list item with the same ID as a previous one overrides it, allowing items to be edited or
// marked as done - the current state of the list is written to a separate part of the journal
// file (or a separate file) on save, so that we can easily edit it on future visits, but the
// entry also contains a changelog of entries added, changed, and removed
condition.mainMenu,
summary.mainMenu,
new Separator(),
journal.mainMenu,
empathyList.mainMenu,
sleep.mainMenu,
// TODO: Modified HierarchicalCheckboxInput to allow for putting more things in the main menu than
// there are letters
// TODO: gratitude journaling
// TODO: thinking about the future - where do you want to be in x years type thing
// TODO: breathing regulation exercises
// TODO: Goals for the day and checking in on how yesterday's goals went
/*
{
name: typeof entry.needs === "object" ? "Check back in on needs" : "Check in on needs",
// TODO: physical needs: food, water, exercise tracking, possibly with autocompletes for each saved in a file like the empathy guide
value: NEEDS,
key: "n"
},
{
name: typeof entry.personas === "object" ? "Check back in on personas" : "Check in on personas",
// TODO: Personas' individual journal entries
value: PERSONA,
key: "p"
},
{name: typeof entry.rpg === "object" ? "Change RPG stats" : "Add RPG stats", value: RPG, key: "r"},
{
name: typeof entry.activities === "object" ? "Change record of recent activities" : "Add record of recent activities",
// TODO: Add general activity set with historical activities saved in a file like the empathy guide
value: ACTIVITIES,
key: "a"
},
{
name: typeof entry.music === "object" ? "Change record of recently played music" : "Add record of recently played music",
// TODO: Add music
value: MUSIC,
key: "m"
},
@ -85,22 +65,16 @@ export function addEntryCommand(): CommandModule {
/*
{
name: typeof entry.recoveries === "object" ? "Try more recovery methods" : "Try some recovery methods",
// TODO: Add list of recovery methods
value: RECOVERIES,
key: "y"
},
*/
]
const promptYaml = yamlPrompt({
inquire,
showError: async (text: string) => console.log(text)
})
const mainMenu = entryMainMenuPrompt({
inquire,
choices: mainMenuItems,
promptYaml
showError: async (value) => console.log(value),
})
const entry = entryPrompt({

@ -1,11 +1,10 @@
import {CommandModule} from "yargs";
import {inquire} from "../prompts/Inquire.js";
import {registerPrompts} from "../prompts/types/index.js";
import {LocalRepository} from "../repository/LocalRepository.js";
import {empathyGroupPrompt} from "../prompts/implementations/EmpathyGroupPrompt.js";
import {empathyGroupListPrompt} from "../prompts/implementations/EmpathyGroupListPrompt.js";
import {empathyGuidePrompt} from "../prompts/implementations/EmpathyGuidePrompt.js";
import {yamlPrompt} from "../schemata/YAMLPrompt.js";
import {inquire} from "../prompts/Inquire";
import {registerPrompts} from "../prompts/types";
import {LocalRepository} from "../repository/LocalRepository";
import {empathyGroupPrompt} from "../prompts/implementations/EmpathyGroupPrompt";
import {empathyGroupListPrompt} from "../prompts/implementations/EmpathyGroupListPrompt";
import {empathyGuidePrompt} from "../prompts/implementations/EmpathyGuidePrompt";
export function updateEmpathyGuideCommand(): CommandModule {
return {
@ -16,14 +15,9 @@ export function updateEmpathyGuideCommand(): CommandModule {
const storage = new LocalRepository()
const empathyGuide = await storage.loadEmpathyGuide()
const promptYaml = yamlPrompt({
inquire,
showError: async (text) => console.error(text),
})
const empathyGroup = empathyGroupPrompt({
inquire,
promptYaml,
showError: async (text) => console.error(text),
})
const empathyList = empathyGroupListPrompt({
@ -33,7 +27,7 @@ export function updateEmpathyGuideCommand(): CommandModule {
const newGuide = await empathyGuidePrompt({
inquire,
promptYaml,
showError: async (text) => console.error(text),
promptForEmpathyGroupList: empathyList,
})({default: empathyGuide})
await storage.saveEmpathyGuide(newGuide)

@ -1,4 +1,4 @@
import {EnumSerializer, OptionalSerializer} from "../schemata/Serialization.js";
import {schema} from "../schemata/SchemaData";
export enum Condition {
CRITICAL = "Critical",
@ -11,5 +11,12 @@ export enum Condition {
}
export const CONDITIONS = [Condition.CRITICAL, Condition.POOR, Condition.SOSO, Condition.GOOD, Condition.EXCELLENT, Condition.UNSURE, Condition.NUMB]
export const ConditionSerializer = new EnumSerializer<Condition>(CONDITIONS)
export const OptionalConditionSerializer = new OptionalSerializer(ConditionSerializer)
export type ConditionJTD = typeof ConditionJTD
export const ConditionJTD = schema({
schema: {
enum: CONDITIONS
},
key: "condition",
references: [],
})

@ -1,4 +1,4 @@
import {schema} from "../schemata/SchemaData.js";
import {schema} from "../schemata/SchemaData";
export interface EmpathyGroup {
readonly header: string

@ -1,4 +1,4 @@
import {EmpathyGroup} from "./EmpathyGroup.js";
import {EmpathyGroup} from "./EmpathyGroup";
export function totalItems(groups: readonly EmpathyGroup[]): number {
return groups.map((group) => group.items.length).reduce((a, b) => a + b)

@ -1,5 +1,5 @@
import {schema} from "../schemata/SchemaData.js";
import {EmpathyGroup, EmpathyGroupJTD} from "./EmpathyGroup.js";
import {schema} from "../schemata/SchemaData";
import {EmpathyGroup, EmpathyGroupJTD} from "./EmpathyGroup";
export interface EmpathyGuide {
readonly feelings?: {

@ -1,16 +1,15 @@
import {Condition, OptionalConditionSerializer} from "./Condition.js";
import {GuidedEmpathy} from "./GuidedEmpathy.js";
import {OptionalSuicidalitySerializer, Suicidality} from "./Suicidality.js";
import {SleepRecord} from "./SleepRecord.js";
import {DateTime} from "luxon";
import {DateTimeSerializer, ObjectSerializer, OptionalStringSerializer} from "../schemata/Serialization.js";
import {Condition, ConditionJTD} from "./Condition";
import {GuidedEmpathy, GuidedEmpathyJTD} from "./GuidedEmpathy";
import {Suicidality, SuicidalityJTD} from "./Suicidality";
import {schema} from "../schemata/SchemaData";
export interface BaseEntry {
readonly condition?: Condition
export interface Entry {
readonly startedAt: Date
readonly finishedAt: Date
readonly condition: Condition
readonly summary?: string
readonly journalEntry?: string
readonly guidedEmpathy?: readonly GuidedEmpathy[]
readonly sleepRecords?: readonly SleepRecord[]
// readonly needs?: Needs
// readonly music?: readonly string[]
// readonly rpg?: RPGStats
@ -19,24 +18,23 @@ export interface BaseEntry {
readonly suicidality?: Suicidality
// readonly recoveries?: readonly Recovery[]
}
export type EntryJTD = typeof EntryJTD
export const EntryJTD = schema({
schema: {
properties: {
startedAt: { type: "timestamp" },
finishedAt: { type: "timestamp" },
condition: ConditionJTD.reference,
},
optionalProperties: {
summary: { type: "string" },
journalEntry: { type: "string" },
guidedEmpathy: { elements: GuidedEmpathyJTD.reference },
suicidality: SuicidalityJTD.reference,
}
},
typeHint: null as Entry|null,
key: "entry",
references: [ConditionJTD, GuidedEmpathyJTD, SuicidalityJTD]
})
export interface Entry extends BaseEntry {
readonly startedAt: DateTime
readonly finishedAt: DateTime
}
export interface SerializedEntry extends BaseEntry {
readonly startedAt: string
readonly finishedAt: string
}
export const EntrySerializer = new ObjectSerializer<Entry, SerializedEntry, keyof Entry>({
startedAt: DateTimeSerializer,
finishedAt: DateTimeSerializer,
condition: OptionalConditionSerializer,
summary: OptionalStringSerializer,
journalEntry: OptionalStringSerializer,
guidedEmpathy: OptionalArrayGuidedEmpathySerializer,
sleepRecords: OptionalArraySleepRecordSerializer,
suicidality: OptionalSuicidalitySerializer,
}, ["startedAt", "finishedAt", "condition", "summary", "journalEntry", "guidedEmpathy", "sleepRecords", "suicidality"])

@ -1,4 +1,4 @@
import {isPopulatedArray} from "../utils/Arrays.js";
import {isPopulatedArray} from "../utils/Arrays";
import {
Persona,
personaName,
@ -6,15 +6,10 @@ import {
personaPronounObject,
personaPronounPossessive,
personaVerb
} from "./Persona.js";
} from "./Persona";
import chalk from "chalk";
import {schema} from "../schemata/SchemaData";
import capitalize from "capitalize";
import {
ArraySerializer,
ObjectSerializer,
OptionalArrayStringSerializer,
OptionalSerializer
} from "../schemata/Serialization.js";
export interface GuidedEmpathy {
readonly feelings?: readonly string[]
@ -22,14 +17,20 @@ export interface GuidedEmpathy {
readonly events?: readonly string[]
readonly requests?: readonly string[]
}
export const GuidedEmpathySerializer = new ObjectSerializer<GuidedEmpathy, GuidedEmpathy, keyof GuidedEmpathy>({
feelings: OptionalArrayStringSerializer,
needs: OptionalArrayStringSerializer,
events: OptionalArrayStringSerializer,
requests: OptionalArrayStringSerializer,
}, ["feelings", "needs", "events", "requests"])
export const ArrayGuidedEmpathySerializer = new ArraySerializer(GuidedEmpathySerializer)
export const OptionalArrayGuidedEmpathySerializer = new OptionalSerializer(ArrayGuidedEmpathySerializer)
export type GuidedEmpathyJTD = typeof GuidedEmpathyJTD
export const GuidedEmpathyJTD = schema({
schema: {
optionalProperties: {
feelings: { elements: { type: "string" } },
needs: { elements: { type: "string" } },
events: { elements: { type: "string" } },
requests: { elements: { type: "string" } },
}
},
typeHint: null as GuidedEmpathy|null,
key: "guidedEmpathy",
references: [],
})
export function isPopulatedGuidedEmpathy(empathy: GuidedEmpathy | undefined): boolean {
return !!empathy && (isPopulatedArray(empathy.feelings)

@ -1,5 +1,5 @@
import {Entry, EntryJTD} from "./Entry.js";
import {schema} from "../schemata/SchemaData.js";
import {Entry, EntryJTD} from "./Entry";
import {schema} from "../schemata/SchemaData";
/* export interface PersonaState {
readonly persona: Persona

@ -1,4 +1,4 @@
import {schema} from "../schemata/SchemaData.js";
import {schema} from "../schemata/SchemaData";
export enum Persona {
HEART = "Heart",

@ -1,96 +0,0 @@
import {schema} from "../schemata/SchemaData.js";
export enum SleepQuality {
ACIDIC = "Acidic",
RESTLESS = "Restless",
NIGHTMARISH = "Nightmarish",
TIMESKIP = "Timeskip",
DREAMLESS = "Dreamless",
DREAMY = "Dreamy",
ECSTASY = "Ecstasy",
}
export const SLEEP_QUALITIES: SleepQuality[] = [SleepQuality.ACIDIC, SleepQuality.RESTLESS, SleepQuality.NIGHTMARISH, SleepQuality.TIMESKIP, SleepQuality.DREAMLESS, SleepQuality.DREAMY, SleepQuality.ECSTASY]
export type SleepQualityJTD = typeof SleepQualityJTD
export const SleepQualityJTD = schema({
schema: {
enum: SLEEP_QUALITIES
},
key: "sleepQuality",
references: [],
})
export enum WakeQuality {
AGONIZED = "Agonized",
EXHAUSTED = "Exhausted",
DROWSY = "Drowsy",
RESTED = "Rested",
ENERGIZED = "Energized",
}
export const WAKE_QUALITIES: WakeQuality[] = [WakeQuality.AGONIZED, WakeQuality.EXHAUSTED, WakeQuality.DROWSY, WakeQuality.RESTED, WakeQuality.ENERGIZED]
export type WakeQualityJTD = typeof WakeQualityJTD
export const WakeQualityJTD = schema({
schema: {
enum: WAKE_QUALITIES
},
key: "wakeQuality",
references: [],
})
export interface SleepRecord {
readonly sleepAt?: Date
readonly sleepQuality?: SleepQuality
readonly interruptions?: number
readonly wakeAt?: Date
readonly wakeQuality?: WakeQuality
readonly dreams?: string
}
export type SleepRecordJTD = typeof SleepRecordJTD
export const SleepRecordJTD = schema({
schema: {
optionalProperties: {
sleepAt: { type: "timestamp" },
sleepQuality: SleepQualityJTD.reference,
interruptions: { type: "uint32" },
wakeAt: { type: "timestamp" },
wakeQuality: WakeQualityJTD.reference,
dreams: { type: "string" },
}
},
typeHint: null as SleepRecord|null,
key: "sleepRecord",
references: [SleepQualityJTD, WakeQualityJTD],
})
export type SleepRecordListJTD = typeof SleepRecordListJTD
export const SleepRecordListJTD = schema({
schema: {
elements: SleepRecordJTD.reference
},
typeHint: null as (readonly SleepRecord[])|null,
key: "sleepRecords",
references: [SleepRecordJTD, ...SleepRecordJTD.requiredReferences],
})
const TimeFormat = Intl.DateTimeFormat([], {
dateStyle: undefined,
timeStyle: undefined,
year: undefined,
month: undefined,
day: undefined,
weekday: undefined,
hour: "numeric",
minute: "2-digit",
second: undefined,
fractionalSecondDigits: undefined,
timeZone: undefined,
hour12: true,
hourCycle: "h12",
})
export function sleepRecordToString(record: SleepRecord) {
const sleepAt = record.sleepAt === undefined ? null : TimeFormat.format(record.sleepAt)
const wakeAt = record.wakeAt === undefined ? null : TimeFormat.format(record.wakeAt)
return `${sleepAt ?? "?:??"} (${record.sleepQuality ?? "???"}) -${record.interruptions ?? "?"}-> ${wakeAt ?? "?:??"} (${record.wakeQuality ?? "???"})${typeof record.dreams === "string" ? " with dream journal" : ""}`
}

@ -1,6 +1,4 @@
import {schema} from "../schemata/SchemaData.js";
import {EnumSerializer, OptionalSerializer} from "../schemata/Serialization.js";
import {Condition, CONDITIONS} from "./Condition.js";
import {schema} from "../schemata/SchemaData";
export enum Suicidality {
NONE = "None",
@ -14,5 +12,12 @@ export enum Suicidality {
}
export const SUICIDALITIES: Suicidality[] = [Suicidality.NONE, Suicidality.PASSIVE, Suicidality.INTRUSIVE, Suicidality.ACTIVE, Suicidality.RESIGNED, Suicidality.PLANNING, Suicidality.PLANNED, Suicidality.DANGER]
export const SuicidalitySerializer = new EnumSerializer<Suicidality>(SUICIDALITIES)
export const OptionalSuicidalitySerializer = new OptionalSerializer(SuicidalitySerializer)
export type SuicidalityJTD = typeof SuicidalityJTD
export const SuicidalityJTD = schema({
schema: {
enum: SUICIDALITIES
},
key: "suicidality",
references: [],
})

@ -1,15 +1,13 @@
import {addEntryCommand} from "./commands/AddEntry.js";
import {updateEmpathyGuideCommand} from "./commands/UpdateEmpathyGuide.js";
import {Argv, default as yargs} from "yargs"
import {addEntryCommand} from "./commands/AddEntry";
import {updateEmpathyGuideCommand} from "./commands/UpdateEmpathyGuide";
import yargs from "yargs"
const Yargs = yargs as unknown as {(): Argv}
Yargs()
yargs
.scriptName("mari-status-bar")
.command(addEntryCommand())
.command(updateEmpathyGuideCommand())
.demandCommand()
.parseAsync()
.catch((err: unknown) => {
.catch((err) => {
console.error(err)
})

@ -1,13 +1,9 @@
import {QuestionMap} from "inquirer";
import inquirer from "inquirer";
import {prompt, QuestionCollection} from "inquirer";
export const prompt = inquirer.prompt;
export const registerPrompt = inquirer.registerPrompt;
export const Separator = inquirer.Separator;
export type InquireFunction<QuestionT extends QuestionMap[keyof QuestionMap], AnswerT extends QuestionT["default"]> = (question: QuestionT) => Promise<AnswerT>
export type InquireFunction = (question: Omit<QuestionCollection, "name">) => Promise<any>
export type ShowFunction = (text: string) => Promise<void>
export async function inquire<QuestionT extends QuestionMap[keyof QuestionMap], AnswerT extends QuestionT["default"]>(question: QuestionT): Promise<AnswerT> {
export async function inquire(question: Omit<QuestionCollection, "name">): Promise<any> {
const result = await prompt([{...question, name: "answer"}])
return result.answer
}

@ -1,16 +1,16 @@
import {ListQuestion} from "inquirer";
import {personaPossessive, PersonaPrompt} from "../../datatypes/Persona.js";
import {ListQuestion, Separator} from "inquirer";
import {personaPossessive, PersonaPrompt} from "../../datatypes/Persona";
import chalk from "chalk";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt.js";
import {asDefault, identity} from "../../utils/Objects.js";
import {InquireFunction, Separator} from "../Inquire.js";
import {Condition} from "../../datatypes/Condition.js";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt";
import {asDefault, identity} from "../../utils/Objects";
import {InquireFunction} from "../Inquire";
import {Condition} from "../../datatypes/Condition";
export interface ConditionPromptOptions extends Partial<Omit<ListQuestion, "name" | "type" | "choices">>, PersonaPrompt {
}
export interface ConditionPromptDependencies {
readonly inquire: InquireFunction<ListQuestion, Condition>
readonly inquire: InquireFunction
}
export function conditionPrompt(deps: ConditionPromptDependencies): {
@ -19,7 +19,7 @@ export function conditionPrompt(deps: ConditionPromptDependencies): {
} {
return makeEntryMainMenuChoice({
property: "condition",
name: (input) => typeof input === "string" ? `Change condition ${chalk.dim(`(currently ${chalk.greenBright(input)})`)}` : `Add condition`,
name: (input) => `Change condition ${chalk.dim(`(currently ${chalk.greenBright(input)})`)}`,
key: "c",
injected: (options) => promptForCondition(options, deps),
toOptions: asDefault,

@ -1,10 +1,9 @@
import {EmpathyGroup} from "../../datatypes/EmpathyGroup.js";
import {isPopulatedArray} from "../../utils/Arrays.js";
import {EmpathyGroup} from "../../datatypes/EmpathyGroup";
import {isPopulatedArray} from "../../utils/Arrays";
import chalk from "chalk";
import pluralize from "pluralize";
import {InquireFunction} from "../Inquire.js";
import {EmpathyGroupPromptOptions} from "./EmpathyGroupPrompt.js";
import {ListQuestion} from "inquirer";
import {InquireFunction} from "../Inquire";
import {EmpathyGroupPromptOptions} from "./EmpathyGroupPrompt";
export interface EmpathyGroupListPromptOptions {
readonly default?: readonly EmpathyGroup[]
@ -12,7 +11,7 @@ export interface EmpathyGroupListPromptOptions {
}
export interface EmpathyGroupListPromptDependencies {
readonly inquire: InquireFunction<ListQuestion, number>
readonly inquire: InquireFunction
readonly promptForEmpathyGroup: (opts: EmpathyGroupPromptOptions) => Promise<EmpathyGroup | null>
}

@ -1,9 +1,8 @@
import capitalize from "capitalize";
import {YamlPromptFunction} from "../../schemata/YAMLPrompt.js";
import {editYaml} from "../../schemata/YAMLPrompt";
import chalk from "chalk";
import {InquireFunction} from "../Inquire.js";
import {EmpathyGroup, EmpathyGroupJTD} from "../../datatypes/EmpathyGroup.js";
import {EditorQuestion, ExpandQuestion, InputQuestion, MultiTextInputQuestion} from "inquirer";
import {InquireFunction, ShowFunction} from "../Inquire";
import {EmpathyGroup, EmpathyGroupJTD} from "../../datatypes/EmpathyGroup";
export interface EmpathyGroupPromptOptions {
default?: EmpathyGroup
@ -11,8 +10,8 @@ export interface EmpathyGroupPromptOptions {
}
export interface EmpathyGroupPromptDependencies {
inquire: InquireFunction<ExpandQuestion, typeof PROMPT|typeof AUTO|typeof YAML> & InquireFunction<InputQuestion, string> & InquireFunction<MultiTextInputQuestion, string[]> & InquireFunction<EditorQuestion, string>
promptYaml: YamlPromptFunction
inquire: InquireFunction
showError: ShowFunction
}
export function empathyGroupPrompt(deps: EmpathyGroupPromptDependencies): (opts: EmpathyGroupPromptOptions) => Promise<EmpathyGroup | null> {
@ -25,7 +24,7 @@ const YAML = "YAML"
export async function promptForEmpathyGroup(opts: EmpathyGroupPromptOptions, deps: EmpathyGroupPromptDependencies): Promise<EmpathyGroup | null> {
const {listName, default: {header = undefined, items = []} = {}} = opts
const {inquire, promptYaml} = deps
const {inquire, showError} = deps
const mode = await inquire({
type: "expand",
message: `How would you like to ${header ? "edit" : "create"} this ${listName} group?`,
@ -52,10 +51,12 @@ export async function promptForEmpathyGroup(opts: EmpathyGroupPromptOptions, dep
items: parsedItems.map((item) => item.toLocaleLowerCase())
}
case YAML:
return await promptYaml({
return await editYaml({
schema: EmpathyGroupJTD,
currentValue: opts.default,
name: `this ${listName} group`,
inquire,
showError,
}) || null
case PROMPT:
default:

@ -1,22 +1,22 @@
import {isPopulatedArray} from "../../utils/Arrays.js";
import {isPopulatedArray} from "../../utils/Arrays";
import chalk from "chalk";
import pluralize from "pluralize";
import {totalItems} from "../../datatypes/EmpathyGroupList.js";
import {ExpandQuestion} from "inquirer";
import {YamlPromptFunction} from "../../schemata/YAMLPrompt.js";
import {InquireFunction, Separator} from "../Inquire.js";
import {EmpathyGroupListPromptOptions} from "./EmpathyGroupListPrompt.js";
import {EmpathyGroup} from "../../datatypes/EmpathyGroup.js";
import {EmpathyGuide, EmpathyGuideJTD} from "../../datatypes/EmpathyGuide.js";
import {totalItems} from "../../datatypes/EmpathyGroupList";
import {Separator} from "inquirer";
import {editYaml} from "../../schemata/YAMLPrompt";
import {InquireFunction, ShowFunction} from "../Inquire";
import {EmpathyGroupListPromptOptions} from "./EmpathyGroupListPrompt";
import {EmpathyGroup} from "../../datatypes/EmpathyGroup";
import {EmpathyGuide, EmpathyGuideJTD} from "../../datatypes/EmpathyGuide";
export interface EmpathyGuidePromptOptions {
readonly default?: EmpathyGuide
}
export interface EmpathyGuidePromptDependencies {
readonly inquire: InquireFunction<ExpandQuestion, typeof PLEASANT | typeof UNPLEASANT | typeof NEEDS | typeof INSPECT | typeof SAVE>
readonly inquire: InquireFunction
readonly showError: ShowFunction
readonly promptForEmpathyGroupList: (opts: EmpathyGroupListPromptOptions) => Promise<readonly EmpathyGroup[]>
readonly promptYaml: YamlPromptFunction
}
export function empathyGuidePrompt(deps: EmpathyGuidePromptDependencies): (opts: EmpathyGuidePromptOptions) => Promise<EmpathyGuide> {
@ -31,7 +31,7 @@ const SAVE = "Save"
export async function promptForEmpathyGuide(opts: EmpathyGuidePromptOptions, deps: EmpathyGuidePromptDependencies): Promise<EmpathyGuide> {
const {default: value} = opts
const {inquire, promptForEmpathyGroupList, promptYaml} = deps
const {inquire, showError, promptForEmpathyGroupList} = deps
const result = await inquire({
type: "expand",
message: "What would you like to modify?",
@ -108,10 +108,12 @@ export async function promptForEmpathyGuide(opts: EmpathyGuidePromptOptions, dep
default:
case INSPECT:
return promptForEmpathyGuide({
default: await promptYaml({
default: await editYaml({
schema: EmpathyGuideJTD,
currentValue: value,
name: "the current empathy guide",
inquire,
showError
})
}, deps)
}

@ -1,18 +1,13 @@
import {InquireFunction} from "../Inquire.js";
import {ExpandChoiceOptions, ExpandQuestion, SeparatorOptions} from "inquirer";
import inquirer from "inquirer";
import {merge} from "../../utils/Merge.js";
import {Entry, EntryJTD} from "../../datatypes/Entry.js";
import {YamlPromptFunction} from "../../schemata/YAMLPrompt.js";
const Separator = inquirer.Separator
import {InquireFunction, ShowFunction} from "../Inquire";
import {ExpandChoiceOptions, Separator, SeparatorOptions} from "inquirer";
import {merge} from "../../utils/Merge";
import {Entry, EntryJTD} from "../../datatypes/Entry";
import {editYaml} from "../../schemata/YAMLPrompt";
const VIEW = ".View"
const DONE = ".Done"
type EntryMainMenuChoiceKey = string & keyof EntryMainMenuOptions
export interface EntryMainMenuChoice<PropertyT extends EntryMainMenuChoiceKey> {
export interface EntryMainMenuChoice<PropertyT extends string & keyof EntryMainMenuOptions> {
readonly type: "mainMenu"
readonly property: PropertyT
choice(input: EntryMainMenuOptions[PropertyT]): ExpandChoiceOptions & { value: PropertyT }
@ -62,9 +57,9 @@ export function makeEntryMainMenuChoice<PropertyT extends string & keyof EntryMa
export interface EntryMainMenuOptions extends Omit<Entry, "finishedAt"> {}
export interface EntryMainMenuDependencies {
readonly inquire: InquireFunction<ExpandQuestion, typeof DONE | typeof VIEW | EntryMainMenuChoiceKey>
readonly choices: readonly (EntryMainMenuChoice<EntryMainMenuChoiceKey>|SeparatorOptions)[]
readonly promptYaml: YamlPromptFunction
readonly inquire: InquireFunction
readonly choices: readonly (EntryMainMenuChoice<any>|SeparatorOptions)[]
readonly showError: ShowFunction
}
export function entryMainMenuPrompt(deps: EntryMainMenuDependencies): (options: EntryMainMenuOptions) => Promise<Entry> {
@ -75,7 +70,7 @@ export async function promptForEntryMainMenu(entry: EntryMainMenuOptions, deps:
const {
inquire,
choices,
promptYaml,
showError,
} = deps
const result = await inquire({
type: "expand",
@ -101,15 +96,17 @@ export async function promptForEntryMainMenu(entry: EntryMainMenuOptions, deps:
finishedAt: new Date(),
}
} else if (result === VIEW) {
const updated = await promptYaml({
const updated = await editYaml({
schema: EntryJTD,
currentValue: {...entry, finishedAt: new Date()},
name: "the current entry",
inquire,
showError,
})
const withoutFinishedAt = {...updated, finishedAt: undefined}
return continueWith(withoutFinishedAt)
} else {
const selectedChoice = choices.find((choice) => choice.type === "mainMenu" && choice.property === result) as EntryMainMenuChoice<string & keyof EntryMainMenuOptions>
const selectedChoice = choices.find((choice) => choice.type === "mainMenu" && choice.property === result) as EntryMainMenuChoice<any>
return continueWith(await onSelected(selectedChoice, entry))
}
}

@ -1,8 +1,8 @@
import {ConditionPromptOptions} from "./ConditionPrompt.js";
import {Condition} from "../../datatypes/Condition.js";
import {SummaryPromptOptions} from "./SummaryPrompt.js";
import {EntryMainMenuOptions} from "./EntryMainMenuPrompt.js";
import {Entry} from "../../datatypes/Entry.js";
import {ConditionPromptOptions} from "./ConditionPrompt";
import {Condition} from "../../datatypes/Condition";
import {SummaryPromptOptions} from "./SummaryPrompt";
import {EntryMainMenuOptions} from "./EntryMainMenuPrompt";
import {Entry} from "../../datatypes/Entry";
interface EntryPromptOptions {
}

@ -1,20 +1,20 @@
import {PersonaPrompt} from "../../datatypes/Persona.js";
import {guidedEmpathyToStringShort, GuidedEmpathy} from "../../datatypes/GuidedEmpathy.js";
import {InquireFunction} from "../Inquire.js";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt.js";
import {PersonaPrompt} from "../../datatypes/Persona";
import {guidedEmpathyToStringShort, GuidedEmpathy} from "../../datatypes/GuidedEmpathy";
import {InquireFunction} from "../Inquire";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt";
import chalk from "chalk";
import pluralize from "pluralize";
import {isPopulatedArray} from "../../utils/Arrays.js";
import {ListChoiceOptions, ListQuestion} from "inquirer";
import {asDefault} from "../../utils/Objects.js";
import {GuidedEmpathyPromptOptions} from "./GuidedEmpathyPrompt.js";
import {isPopulatedArray} from "../../utils/Arrays";
import {ListChoiceOptions} from "inquirer";
import {asDefault} from "../../utils/Objects";
import {GuidedEmpathyPromptOptions} from "./GuidedEmpathyPrompt";
export interface GuidedEmpathyListPromptOptions extends PersonaPrompt {
readonly default?: readonly GuidedEmpathy[]
}
export interface GuidedEmpathyListPromptDependencies {
readonly inquire: InquireFunction<ListQuestion, number>
readonly inquire: InquireFunction
readonly promptForEmpathy: (input: GuidedEmpathyPromptOptions) => Promise<GuidedEmpathy|null>
}

@ -4,21 +4,21 @@ import {
personaPronounObject,
personaPronounPossessive,
personaVerb
} from "../../datatypes/Persona.js";
import {InquireFunction, ShowFunction} from "../Inquire.js";
import {EmpathyGuide} from "../../datatypes/EmpathyGuide.js";
import {isPopulatedArray} from "../../utils/Arrays.js";
import {Separator} from "../Inquire.js";
import {EmpathyGroup} from "../../datatypes/EmpathyGroup.js";
import {GuidedEmpathy, guidedEmpathyToString, isPopulatedGuidedEmpathy} from "../../datatypes/GuidedEmpathy.js";
import {HierarchicalCheckboxChildChoice, HierarchicalCheckboxParentChoice, HierarchicalCheckboxQuestion, MultiTextInputQuestion } from "inquirer";
} from "../../datatypes/Persona";
import {InquireFunction, ShowFunction} from "../Inquire";
import {EmpathyGuide} from "../../datatypes/EmpathyGuide";
import {isPopulatedArray} from "../../utils/Arrays";
import Separator from "inquirer/lib/objects/separator";
import {EmpathyGroup} from "../../datatypes/EmpathyGroup";
import {GuidedEmpathy, guidedEmpathyToString, isPopulatedGuidedEmpathy} from "../../datatypes/GuidedEmpathy";
import {HierarchicalCheckboxChildChoice, HierarchicalCheckboxParentChoice } from "inquirer";
export interface GuidedEmpathyPromptOptions extends PersonaPrompt {
readonly default?: GuidedEmpathy
}
export interface GuidedEmpathyPromptDependencies {
readonly inquire: InquireFunction<HierarchicalCheckboxQuestion, readonly string[]> & InquireFunction<MultiTextInputQuestion, readonly string[]>
readonly inquire: InquireFunction,
readonly guideFactory: () => EmpathyGuide,
readonly show: ShowFunction,
}

@ -1,16 +1,16 @@
import {EditorQuestion} from "inquirer";
import {InquireFunction} from "../Inquire.js";
import {personaPossessive, PersonaPrompt} from "../../datatypes/Persona.js";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt.js";
import {InquireFunction} from "../Inquire";
import {personaPossessive, PersonaPrompt} from "../../datatypes/Persona";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt";
import chalk from "chalk";
import pluralize from "pluralize";
import wordcount from "wordcount";
import {asDefault} from "../../utils/Objects.js";
import {asDefault} from "../../utils/Objects";
export interface JournalEntryPromptOptions extends Partial<Omit<EditorQuestion, "name" | "type">>, PersonaPrompt {}
export interface JournalEntryPromptDependencies {
readonly inquire: InquireFunction<EditorQuestion, string>
readonly inquire: InquireFunction
}
export function journalEntryPrompt(deps: JournalEntryPromptDependencies): {

@ -1,196 +0,0 @@
import {
DateQuestion,
EditorQuestion,
ListQuestion,
NumberQuestion,
} from "inquirer";
import {Separator} from "../Inquire.js";
import {InquireFunction} from "../Inquire.js";
import {SleepQuality, SleepRecord, sleepRecordToString, WakeQuality} from "../../datatypes/SleepRecord.js";
import {DateTime} from "luxon";
import chalk from "chalk";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt.js";
export interface SleepRecordPromptOptions extends Partial<Omit<EditorQuestion, "name"|"type"|"default"> & Omit<DateQuestion, "name"|"type"|"default"> & Omit<ListQuestion, "name"|"type"|"default"> & Omit<NumberQuestion, "name"|"type"|"default">>{
default?: SleepRecord,
}
export interface SleepRecordPromptDependencies {
readonly inquire: InquireFunction<EditorQuestion, string> & InquireFunction<DateQuestion, Date|null> & InquireFunction<ListQuestion & {default: SleepQuality|undefined}, SleepQuality|undefined> & InquireFunction<ListQuestion & {default: WakeQuality|undefined}, WakeQuality|undefined> & InquireFunction<NumberQuestion, number>
}
export function sleepRecordPrompt(deps: SleepRecordPromptDependencies): {
(options: SleepRecordPromptOptions): Promise<SleepRecord|null>,
mainMenu: EntryMainMenuChoice<"sleepRecords">,
} {
return makeEntryMainMenuChoice({
property: "sleepRecords",
name: (input) => typeof input !== "object" || input.length === 0 ? `Add sleep record` : `Change sleep record ${chalk.dim(`(currently ${chalk.greenBright(sleepRecordToString(input[0]))})`)}`,
key: "z",
injected: (options) => promptForSleepRecord(options, deps),
toOptions: (value) => typeof value !== "object" || value.length === 0 ? {} : {default: value[0]},
toProperty: (value) => value === null ? undefined : [value],
})
}
export async function promptForSleepRecord(options: SleepRecordPromptOptions, {inquire}: SleepRecordPromptDependencies): Promise<SleepRecord | null> {
const oldBedTime = options.default?.sleepAt
const newBedTime = await inquire({
...options,
type: "date",
message: "About when did you get to sleep?",
clearable: true,
startCleared: false,
default: oldBedTime ?? DateTime.local().set({hour: 23, minute: 0, second: 0, millisecond: 0}).minus({days: 1}).toJSDate(),
format: {
year: undefined,
weekday: "short",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: undefined,
fractionalSecondDigits: undefined,
timeZoneName: "short",
},
startingDatePart: "hour",
deltas: {
weekday: 1,
hour: [1, 5, 10],
minute: [15, 5, 1],
default: 0
}
})
const oldSleepQuality = options.default?.sleepQuality
const newSleepQuality = await inquire({
...options,
type: "list",
message: `How'd you sleep?`,
choices: [
{
value: SleepQuality.ACIDIC,
name: `Acidic ${chalk.dim("(extremely poor/extremely restless/heavy acid reflux)")}`
},
{
value: SleepQuality.RESTLESS,
name: `Restless ${chalk.dim("(very poor/very restless/fever dreamy)")}`
},
{
value: SleepQuality.NIGHTMARISH,
name: `Nightmarish ${chalk.dim("(poor/restless/plagued with nightmares)")}`
},
{
value: SleepQuality.TIMESKIP,
name: `Timeskip ${chalk.dim("(neutral/passage of time not recognized)")}`
},
{
value: SleepQuality.DREAMLESS,
name: `Dreamless ${chalk.dim("(good/peaceful/no dreams had or remembered)")}`
},
{
value: SleepQuality.DREAMY,
name: `Dreamy ${chalk.dim("(very good/peaceful/neutral to somewhat pleasant dreams)")}`
},
{
value: SleepQuality.ECSTASY,
name: `Ecstasy ${chalk.dim("(extremely good/very peaceful/very pleasant dreams)")}`
},
new Separator(),
{
value: undefined,
name: `Uncertain`
},
],
default: oldSleepQuality ?? SleepQuality.TIMESKIP,
pageSize: 999,
})
const oldInterruptions = options.default?.interruptions
const newInterruptionsRaw = await inquire({
...options,
type: "number",
message: "How many times did you end up waking up during the sleep?",
default: oldInterruptions ?? -1,
})
const newInterruptions = newInterruptionsRaw < 0 ? undefined : Math.round(newInterruptionsRaw)
const oldWakeTime = options.default?.wakeAt
const newWakeTime = await inquire({
...options,
type: "date",
message: "Around when did you get up?",
clearable: true,
startCleared: false,
default: oldWakeTime ?? DateTime.local().set({hour: 7, minute: 0, second: 0, millisecond: 0}).toJSDate(),
format: {
year: undefined,
weekday: "short",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: undefined,
fractionalSecondDigits: undefined,
timeZoneName: "short",
},
startingDatePart: "hour",
deltas: {
hour: [1, 5, 10],
minute: [15, 5, 1],
default: 0
}
})
const oldWakeQuality = options.default?.wakeQuality
const newWakeQuality = await inquire({
...options,
type: "list",
message: `How'd you feel when you got up?`,
choices: [
{
value: WakeQuality.AGONIZED,
name: `Agonized ${chalk.dim("(in physical pain/barely able or unable to get out of bed)")}`
},
{
value: WakeQuality.EXHAUSTED,
name: `Exhausted ${chalk.dim("(very tired/not wanting to get out of bed)")}`
},
{
value: WakeQuality.DROWSY,
name: `Drowsy ${chalk.dim("(a bit sleepy but willing to get out of bed)")}`
},
{
value: WakeQuality.RESTED,
name: `Rested ${chalk.dim("(well rested and ready to get going)")}`
},
{
value: WakeQuality.ENERGIZED,
name: `Energized ${chalk.dim("(thoroughly recharged and eager to get up and running)")}`
},
new Separator(),
{
value: undefined,
name: `Uncertain`
},
],
default: oldWakeQuality ?? WakeQuality.DROWSY,
pageSize: 999,
})
const oldDreamJournal = options.default?.dreams
const newDreamJournalRaw = (await inquire({
...options,
type: "editor",
message: typeof oldDreamJournal === "string" ? `Edit dream journal for this sleep session:` : `Type up dream journal for this sleep session:`,
default: oldDreamJournal,
})).trimEnd()
const newDreamJournal = newDreamJournalRaw === "" ? undefined : newDreamJournalRaw
const result: SleepRecord = {
dreams: newDreamJournal,
interruptions: newInterruptions,
sleepAt: newBedTime ?? undefined,
sleepQuality: newSleepQuality,
wakeAt: newWakeTime ?? undefined,
wakeQuality: newWakeQuality,
}
return (
Object.keys(result).map((key: keyof SleepRecord) => result[key]).some(value => (value !== undefined))
? result
: null)
}

@ -1,15 +1,15 @@
import {ListQuestion} from "inquirer";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt.js";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt";
import chalk from "chalk";
import {asDefault, identity} from "../../utils/Objects.js";
import {InquireFunction} from "../Inquire.js";
import {Suicidality} from "../../datatypes/Suicidality.js";
import {asDefault, identity} from "../../utils/Objects";
import {InquireFunction} from "../Inquire";
import {Suicidality} from "../../datatypes/Suicidality";
export interface SuicidalityPromptOptions extends Partial<Omit<ListQuestion, "name" | "type" | "choices">> {
}
export interface SuicidalityPromptDependencies {
inquire: InquireFunction<ListQuestion, Suicidality>
inquire: InquireFunction
}
export function suicidalityPrompt(deps: SuicidalityPromptDependencies): {

@ -1,16 +1,16 @@
import {InputQuestion} from "inquirer";
import {InquireFunction} from "../Inquire.js";
import {personaName, personaPossessive, PersonaPrompt, personaVerb} from "../../datatypes/Persona.js";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt.js";
import {InquireFunction} from "../Inquire";
import {personaName, personaPossessive, PersonaPrompt, personaVerb} from "../../datatypes/Persona";
import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPrompt";
import chalk from "chalk";
import pluralize from "pluralize";
import wordcount from "wordcount";
import {asDefault} from "../../utils/Objects.js";
import {asDefault} from "../../utils/Objects";
export interface SummaryPromptOptions extends Partial<Omit<InputQuestion, "name" | "type">>, PersonaPrompt {}
export interface SummaryPromptDependencies {
readonly inquire: InquireFunction<InputQuestion, string>
readonly inquire: InquireFunction
}
export function summaryPrompt(deps: SummaryPromptDependencies): {

@ -1,386 +0,0 @@
/*
* Adapted from https://github.com/haversnail/inquirer-date-prompt:
* MIT License
*
* Copyright (c) 2021 Alex Havermale
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import chalk from "chalk";
import {DateTime, DurationObjectUnits} from "luxon";
import inquirer, {Answers, DateQuestion, Question} from "inquirer";
import {Interface as ReadLineInterface, Key} from "readline";
import Prompt from "inquirer/lib/prompts/base.js";
import observe from "inquirer/lib/utils/events.js";
import {map, takeUntil} from "rxjs/operators/index.js";
import cliCursor from "cli-cursor"
import SuccessfulPromptStateData = inquirer.prompts.SuccessfulPromptStateData;
import FailedPromptStateData = inquirer.prompts.FailedPromptStateData;
declare module "inquirer" {
export interface DateQuestionOptions<AnswerT extends Answers> {
/**
* Transforms the value to display to the user.
*
* @param date
* The currently selected date in string format.
*
* @param answers
* The answers provided by the users.
*
* @param flags
* Additional information about the value.
*
* @returns
* The value to display to the user.
*/
transformer?(
date: string,
answers: AnswerT,
flags: { isDirty?: boolean; isCleared?: boolean; isFinal?: boolean },
): string | Promise<string>;
/**
* A Boolean value indicating whether the prompt is clearable.
* If `true`, pressing `backspace` or `delete` will replace the current value with `null`.
*/
clearable?: boolean;
/**
* A Boolean value indicating whether the prompt should start cleared even though a default is provided.
* If `true`, the value will start off null, but clearing it will change to the default date.
*/
startCleared?: boolean;
/**
* A specific locale to use when formatting the date.
* If no locale is provided, it will default to the user's current locale.
* @see the {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat|Intl.DateTimeFormat} docs for more info.
*/
locale?: string;
/**
* A set of options for customizing the date format.
* @see the {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat|Intl.DateTimeFormat} docs for more info.
*/
format?: Intl.DateTimeFormatOptions;
/** The date part that should be highlighted when the prompt is shown. */
startingDatePart?: Intl.DateTimeFormatPartTypes
/** The amount by which each value should change. If null, the value will not be selectable. */
deltas?: {
[key in Intl.DateTimeFormatPartTypes|"default"]?: number|[number]|[number,number]|[number,number,number]|[number,number,number,number]|0
}
}
export interface DateQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, DateQuestionOptions<AnswerT> {
/** @inheritDoc */
type: "date"
/** If the default is not provided or null, the value will be the present date at the time that the prompt is shown. */
default?: Date|null
}
export interface QuestionMap {
["date"]: DateQuestion
}
}
/**
* A lookup object that maps each date part type to the corresponding field of a duration.
*/
const offsetLookup: Partial<Record<Intl.DateTimeFormatPartTypes, keyof DurationObjectUnits>> = {
year: "years",
month: "months",
day: "days",
hour: "hours",
minute: "minutes",
second: "seconds",
weekday: "days",
};
/**
* Returns the index of the _last_ element in the array where predicate is true, and -1 otherwise.
*/
function findLastIndex<T>(array: T[], predicate: (value: T, index: number, obj: T[]) => boolean) {
let l = array.length;
while (l--) {
if (predicate(array[l], l, array)) return l;
}
return -1;
}
/**
* Represents a date prompt.
*/
export class DateInput<AnswerT extends Answers = Answers, QuestionT extends DateQuestion<AnswerT> = DateQuestion> extends Prompt<QuestionT> {
date: DateTime
readonly transformer: DateQuestion["transformer"]
readonly clearable: boolean
readonly format: Intl.DateTimeFormatOptions
readonly deltas: NonNullable<DateQuestion["deltas"]>
done: ((state: unknown) => void)|null = null
isDirty: boolean
isCleared: boolean
cursorIndex: number
firstEditableIndex: number
lastEditableIndex: number
constructor(questions: QuestionT, rl: ReadLineInterface, answers: AnswerT) {
super(questions, rl, answers);
// Set the format object based on the user's specified options:
const { transformer, clearable, startCleared, locale, format = {}, default: date, deltas, startingDatePart } = this.opt;
this.transformer = transformer
this.deltas = deltas ?? {}
this.clearable = clearable ?? false
this.format = {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
...format,
};
// Set the date object with either the default value or the current date:
this.date = DateTime.fromJSDate(date ?? new Date());
if (typeof locale === "string") {
this.date = this.date.setLocale(locale)
}
// Clear the default value option (so it won't be printed by the Prompt class):
this.opt.default = null;
this.isDirty = false;
this.isCleared = startCleared ?? false;
// Set the first and last indices of the editable date parts:
this.firstEditableIndex = this.dateParts.findIndex((part) => this.isDatePartEditable(part.type));
this.lastEditableIndex = findLastIndex(this.dateParts, (part) => this.isDatePartEditable(part.type));
// Set the cursor index to the first editable part:
this.cursorIndex = !!startingDatePart && this.isDatePartEditable(startingDatePart) ? this.dateParts.findIndex((part) => part.type === startingDatePart) : this.firstEditableIndex;
}
// Called by parent class:
_run(cb: DateInput["done"]) {
this.done = cb;
// Observe events:
const events = observe(this.rl);
const submit = events.line.pipe(map(() => (this.isCleared ? null : this.date.toJSDate())));
const validation = this.handleSubmitEvents(submit);
validation.success.forEach(this.onEnd.bind(this));
validation.error.forEach(this.onError.bind(this));
events.keypress.pipe(takeUntil(validation.success)).forEach(this.onKeypress.bind(this));
// Init the prompt:
cliCursor.hide();
this.render();
return this;
}
/**
* Renders the prompt.
* @param {string} [error]
*/
render(error?: string) {
let message = this.getQuestion(); // The question portion of the output, including any prefix and suffix
const { isDirty, isCleared } = this;
const isFinal = this.status === "answered";
if (!isCleared) {
const dateString = this.dateParts
.map(({ value }, index) =>
isFinal
? chalk.cyan(value)
: index === this.cursorIndex
? chalk.inverse(value)
: !isDirty
? chalk.dim(value)
: value,
)
.join("");
// Apply the transformer function if one was provided:
message += this.opt.transformer
? this.opt.transformer(dateString, this.answers as AnswerT, { isDirty, isCleared, isFinal })
: dateString;
// Display info on how to clear if the prompt is clearable:
if (this.opt.clearable && !isFinal) {
message += chalk.dim(" (<delete> to clear) ");
}
}
const bottomContent = error ? chalk.red(">> ") + error : "";
// Render the final message:
this.screen.render(message, bottomContent);
}
/**
* The end event handler.
*/
onEnd({ value }: SuccessfulPromptStateData<Date|null>) {
this.status = "answered";
// Re-render prompt
this.render();
this.screen.done();
cliCursor.show();
if (this.done !== null) {
this.done(value);
}
}
/**
* The error event handler.
*/
onError({ isValid }: FailedPromptStateData) {
this.render(isValid || undefined);
}
/**
* The array of date part objects according to the user's specified format.
*/
get dateParts() {
return this.date.toLocaleParts(this.format);
}
/**
* The currently selected date part.
*/
get currentDatePart() {
return this.dateParts[this.cursorIndex];
}
isDatePartEditable(part: Intl.DateTimeFormatPartTypes): boolean {
return offsetLookup.hasOwnProperty(part) && ((this.deltas[part] ?? this.deltas["default"] ?? 1) !== 0)
}
/**
* A Boolean value indicating whether the currently selected date part is editable.
*/
get isCurrentDatePartEditable() {
return this.isDatePartEditable(this.currentDatePart.type);
}
/**
* Moves the cursor index to the right.
*/
incrementCursorIndex() {
if (this.cursorIndex < this.lastEditableIndex) {
this.cursorIndex++;
}
}
/**
* Moves the cursor index to the left.
*/
decrementCursorIndex() {
if (this.cursorIndex > this.firstEditableIndex) {
this.cursorIndex--;
}
}
/**
* Shifts the currently selected date part to the specified offset value.
* The default value is `0`.
* @param {number} offset
*/
shiftDatePartValue(offset = 0) {
const { type } = this.currentDatePart;
const duration: DurationObjectUnits = {}
const offsetProperty = offsetLookup[type]
if (offset !== 0 && typeof offsetProperty === "string") {
duration[offsetProperty] = offset
// Set the input as "dirty" now that the initial date is being changed:
this.isDirty = true;
this.date = this.date.plus(duration)
}
}
/**
* Increments the currently selected date part by one.
*/
incrementDatePartValueBy(value = 1) {
this.shiftDatePartValue(value);
}
/**
* Decrements the currently selected date part by one.
*/
decrementDatePartValueBy(value = 1) {
this.shiftDatePartValue(-1 * value);
}
/**
* The keypress event handler.
*/
onKeypress({ key }: {key: Key}) {
// Reset cleared state if any other key is pressed:
if (this.isCleared) {
this.isCleared = false;
this.isDirty = true;
return
}
// Calculate the amount to increment/decrement by based on modifiers:
const deltas = this.deltas[this.currentDatePart.type] ?? this.deltas["default"] ?? [1, 10, 100]
const amount = ((): number => {
if (typeof deltas === "number") {
return deltas
} else {
switch (deltas.length) {
case 1:
return deltas[0]
case 2:
return (key.shift || key.meta) ? deltas[1] : deltas[0]
case 3:
return (key.shift || key.meta) ? (key.shift && key.meta) ? deltas[2] : deltas[1] : deltas[0]
case 4:
return key.shift ? key.meta ? deltas[3] : deltas[1] : key.meta ? deltas[2] : deltas[0]
}
}
})()
switch (key.name) {
case "right":
do {
this.incrementCursorIndex();
} while (!this.isCurrentDatePartEditable); // increments the cursor index until it hits an editable value
break;
case "left":
do {
this.decrementCursorIndex();
} while (!this.isCurrentDatePartEditable); // decrements the cursor index until it hits an editable value
break;
case "up":
this.incrementDatePartValueBy(amount);
break;
case "down":
this.decrementDatePartValueBy(amount);
break;
case "delete":
case "backspace":
if (this.clearable) this.isCleared = true;
break;
}
this.render();
}
}

@ -1,26 +1,22 @@
import Prompt from "inquirer/lib/prompts/base.js";
import observe from "inquirer/lib/utils/events.js";
import Prompt from "inquirer/lib/prompts/base";
import observe from "inquirer/lib/utils/events";
import {
Answers,
ChoiceOptions,
HierarchicalCheckboxChoice,
HierarchicalCheckboxQuestion,
Question,
Separator,
SeparatorOptions,
} from "inquirer";
import {Separator} from "../Inquire.js";
import {Interface as ReadLineInterface} from "readline";
import {filter} from "rxjs/operators/index.js"
import {filter} from "rxjs/operators"
import chalk from "chalk";
import {Subject} from "rxjs";
import figures from "figures";
import Paginator from "inquirer/lib/utils/paginator.js";
import {isPopulatedArray} from "../../utils/Arrays.js";
import {FilterOptions} from "fuzzy";
import fuzzy from "fuzzy";
import ScreenManager from "inquirer/lib/utils/screen-manager.js";
const fuzzyFilter = fuzzy.filter;
import Paginator from "inquirer/lib/utils/paginator";
import {isPopulatedArray} from "../../utils/Arrays";
import {filter as fuzzyFilter, FilterOptions} from "fuzzy";
interface ExtendedReadLine extends ReadLineInterface {
line: string
@ -55,7 +51,7 @@ declare module "inquirer" {
export interface HierarchicalCheckboxQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, HierarchicalCheckboxQuestionOptions {
/** @inheritDoc */
type: "hierarchical-checkbox"
default?: readonly string[]
default?: string[]
}
export interface QuestionMap {
@ -63,14 +59,6 @@ declare module "inquirer" {
}
}
interface ExtendedPaginatorConstructor {
new(manager: ScreenManager, options: {isInfinite:boolean}): ExtendedPaginator
}
interface ExtendedPaginator extends Paginator {
paginate(content: string, selectedIndex: number, pageSize?: number): string;
}
interface NormalizedChoiceBase {
readonly name: string
readonly value: string|null
@ -272,17 +260,14 @@ export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, Questi
this.updateUnlistedValues()
}
_run(callback: Exclude<HierarchicalCheckboxInput["done"], null>, error?: (err: Error) => void) {
// @ts-ignore
_run(callback: HierarchicalCheckboxInput["done"], error: (err: Error) => void) {
const invalidChoices = Object.keys(this.invalidValues);
const invalidValues = this.value.filter((value) => !this.valueMap.hasOwnProperty(value));
if (isPopulatedArray(invalidChoices)) {
if (error) {
error(Error(`Duplicate values: ${invalidChoices.join()}`))
}
error(Error(`Duplicate values: ${invalidChoices.join()}`))
} else if (isPopulatedArray(invalidValues)) {
if (error) {
error(Error(`Unknown values: ${invalidValues.join()}`))
}
error(Error(`Unknown values: ${invalidValues.join()}`))
}
this.rl.on("history", (history) => {
history.splice(0, history.length)
@ -309,12 +294,13 @@ export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, Questi
private readonly valueMap: ValidValueMap
private readonly invalidValues: InvalidValueMap
private readonly value: string[]
private done: ((state: unknown) => void)|null
private done: (state: any) => void|null
private location: Breadcrumb
private activeIndex: number
private unlistedValues: string[]
private submitter: Subject<string[]> = new Subject<string[]>()
private paginator: ExtendedPaginator = new (Paginator as ExtendedPaginatorConstructor)(this.screen, {isInfinite: true});
// @ts-ignore
private paginator: Paginator = new Paginator(this.screen, {isInfinite: true});
private scheduledRender: NodeJS.Immediate|null = null
private get readline(): ExtendedReadLine {
@ -577,7 +563,7 @@ export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, Questi
children: this.readline.line === "" ? this.location.searchable : fuzzyFilter(this.readline.line, this.location.searchable.slice(), {
pre: searchHighlightPrefix,
post: searchHighlightSuffix,
extract(input: NormalizedLeafChoice | NormalizedParentChoice): string {
extract(input: any): string {
return input.name
}
} as FilterOptions<NormalizedParentChoice | NormalizedLeafChoice>).map((result) => ({
@ -658,9 +644,7 @@ export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, Questi
this.status = "answered"
this.render()
this.screen.done()
if (this.done !== null) {
this.done(value)
}
this.done(value)
}
private onOtherKey() {

@ -1,12 +1,12 @@
import Prompt from "inquirer/lib/prompts/base.js";
import observe from "inquirer/lib/utils/events.js";
import Prompt from "inquirer/lib/prompts/base";
import observe from "inquirer/lib/utils/events";
import {Answers, MultiTextInputQuestion, Question} from "inquirer";
import {Interface as ReadLineInterface} from "readline";
import {filter} from "rxjs/operators/index.js"
import {filter} from "rxjs/operators"
import chalk from "chalk";
import {Subject} from "rxjs";
import figures from "figures";
import Paginator from "inquirer/lib/utils/paginator.js";
import Paginator from "inquirer/lib/utils/paginator";
interface ExtendedReadLine extends ReadLineInterface {
line: string
@ -21,11 +21,11 @@ declare module "inquirer" {
export interface MultiTextInputQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, MultiTextInputQuestionOptions {
/** @inheritDoc */
type: "multitext"
default?: readonly string[]
default?: string[]
}
export interface QuestionMap {
["multitext"]: MultiTextInputQuestion
["multitext"]: HierarchicalCheckboxQuestion
}
}
@ -60,7 +60,7 @@ export class MultiTextInput<AnswerT extends Answers = Answers, QuestionT extends
this.scheduleRender()
}
private done: (state: unknown) => void|null
private done: (state: any) => void|null
private activeIndex: number
private readonly value: string[]
private submitter: Subject<string[]> = new Subject<string[]>()

@ -1,10 +1,8 @@
import {registerPrompt} from "../Inquire.js";
import {MultiTextInput} from "./MultiTextInput.js";
import {HierarchicalCheckboxInput} from "./HierarchicalCheckboxInput.js";
import {DateInput} from "./DateInput.js";
import {registerPrompt} from "inquirer";
import {MultiTextInput} from "./MultiTextInput";
import {HierarchicalCheckboxInput} from "./HierarchicalCheckboxInput";
export function registerPrompts() {
registerPrompt("multitext", MultiTextInput)
registerPrompt("hierarchical-checkbox", HierarchicalCheckboxInput)
registerPrompt("date", DateInput)
}

@ -1,18 +1,18 @@
import {EmpathyGuide, EmpathyGuideJTD} from "../datatypes/EmpathyGuide.js";
import {Entry} from "../datatypes/Entry.js";
import {EmpathyGuide, EmpathyGuideJTD} from "../datatypes/EmpathyGuide";
import {Entry} from "../datatypes/Entry";
import {
ReferencedSchema,
ReferencedTypes,
schema,
SchemaData,
AnyReferenceList, Value
} from "../schemata/SchemaData.js";
UntypedReferenceList
} from "../schemata/SchemaData";
import {rm, open, FileHandle} from "fs/promises";
import {join, dirname} from "path";
import {dump, load} from "js-yaml";
import envPaths from "env-paths";
import makeDir from "make-dir";
import {Journal, JournalJTD} from "../datatypes/Journal.js";
import {Journal, JournalJTD} from "../datatypes/Journal";
export type AutosaveFile = Record<string, unknown>
export type AutosaveFileJTD = typeof AutosaveFileJTD
@ -25,12 +25,6 @@ export const AutosaveFileJTD = schema({
references: []
})
export interface LocalRepositoryDependencies {
rm: typeof rm
open: typeof open
makeDir: typeof makeDir
}
export class LocalRepository {
private readonly paths: envPaths.Paths
@ -126,11 +120,10 @@ export class LocalRepository {
}
}
// TODO: Enable autosave for all commands
async loadAutosaveObjects<TypesT extends AnyReferenceList>(
async loadAutosaveObjects<TypesT extends UntypedReferenceList>(
{autosaveNamespace, schemas}: { autosaveNamespace: string, schemas: TypesT }): Promise<{
validated: Partial<ReferencedTypes<TypesT>>,
unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, string>>
unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, unknown>>
}> {
const path = this.getAutosaveFilePath(autosaveNamespace)
let handle: FileHandle
@ -150,14 +143,14 @@ export class LocalRepository {
throw Error(`Autosave file ${autosaveNamespace} was too corrupted to make sense of`)
}
const validated: Partial<ReferencedTypes<TypesT>> = {}
const unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, string>> = {}
const unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, unknown>> = {}
schemas.forEach((schema: ReferencedSchema<TypesT>) => {
if (autosaved.hasOwnProperty(schema.key)) {
const value = autosaved[schema.key]
if (!schema.validate(value)) {
unvalidated[schema.key as keyof ReferencedTypes<TypesT>] = value
} else {
validated[schema.key as keyof ReferencedTypes<TypesT>] = value as Value<ReferencedSchema<TypesT>>
validated[schema.key as keyof ReferencedTypes<TypesT>] = value
}
}
})
@ -167,7 +160,7 @@ export class LocalRepository {
}
}
async saveAutosaveObject<T>({schema, value, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<T, string, AnyReferenceList, ReferencedTypes<AnyReferenceList>>, value: T}): Promise<void> {
async saveAutosaveObject<T>({schema, value, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<T, any, any, any>, value: T}): Promise<void> {
const path = this.getAutosaveFilePath(autosaveNamespace)
await makeDir(dirname(path))
const handle = await open(path, "a+")
@ -185,7 +178,7 @@ export class LocalRepository {
}
}
async clearAutosaveObject({schema, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<unknown, string, AnyReferenceList, ReferencedTypes<AnyReferenceList>>}): Promise<void> {
async clearAutosaveObject({schema, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<any, any, any, any>}): Promise<void> {
const path = this.getAutosaveFilePath(autosaveNamespace)
await makeDir(dirname(path))
const handle = await open(path, "a+")

@ -1,57 +1,48 @@
import AJV, {ValidateFunction, JTDSchemaType} from "ajv/dist/jtd.js";
import AJV, {ValidateFunction, JTDSchemaType} from "ajv/dist/jtd";
const ajv = new AJV();
interface BaseSchemaData<RepresentedT, ReferenceT extends string> {
value?: RepresentedT,
schema: unknown,
key: ReferenceT,
definition: { [key in ReferenceT]: unknown }
reference: { "ref": ReferenceT }
validate: ValidateFunction<RepresentedT>
requiredReferences: AnyReferenceList
}
export interface SchemaData<RepresentedT, ReferenceT extends string, ReferencesT extends AnyReferenceList, DefinitionsT extends ReferencedTypes<ReferencesT>> extends BaseSchemaData<RepresentedT, ReferenceT> {
export type SchemaData<RepresentedT, ReferenceT extends string, ReferencesT extends UntypedReferenceList, DefinitionsT extends ReferencedTypes<ReferencesT>> = {
value?: RepresentedT,
schema: JTDSchemaType<RepresentedT, DefinitionsT>,
key: ReferenceT,
definition: { [key in ReferenceT]: JTDSchemaType<RepresentedT, DefinitionsT> }
referenced?: { [key in ReferenceT]: RepresentedT }
reference: { "ref": ReferenceT },
validate: ValidateFunction<RepresentedT>
requiredReferences: ReferencesT
}
export type AnySchemaDataFor<RepresentedT> = BaseSchemaData<RepresentedT, string>
export type AnySchemaData = AnySchemaDataFor<unknown>
export type AnyReferenceList = AnySchemaData[]
export type AnyDefinitions = Record<string, unknown>
export type UntypedReferenceList = SchemaData<any, any, any, any>[]
export type Schema<DataT extends AnySchemaData> = Exclude<DataT["schema"], undefined>
export type Value<DataT extends AnySchemaData> = Exclude<DataT["value"], undefined>
export type Definition<DataT extends AnySchemaData> = Exclude<DataT["definition"], undefined>
export type Reference<DataT extends AnySchemaData> = Exclude<DataT["reference"], undefined>
export type ReferenceKey<DataT extends AnySchemaData> = Exclude<DataT["key"], undefined>
export type Schema<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["schema"], undefined>
export type Value<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["value"], undefined>
export type Definition<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["definition"], undefined>
export type Reference<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["reference"], undefined>
export type ReferenceKey<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["key"], undefined>
export type Referenced<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["referenced"], undefined>
export type ReferencedSchemaMap<ReferencesT extends AnyReferenceList> = {
[Property in keyof ReferencesT as ReferencesT[Property] extends AnySchemaData ? ReferenceKey<ReferencesT[Property]> : never]: ReferencesT[Property] extends AnySchemaData ? ReferencesT[Property] : never
export type ReferencedSchemaMap<ReferencesT extends UntypedReferenceList> = {
[Property in keyof ReferencesT as ReferencesT[Property] extends SchemaData<any, any, any, any> ? ReferenceKey<ReferencesT[Property]> : never]: ReferencesT[Property] extends SchemaData<any, any, any, any> ? ReferencesT[Property] : never
}
export type ReferencedSchema<ReferencesT extends UntypedReferenceList> = ReferencedSchemaMap<ReferencesT>[keyof ReferencedSchemaMap<ReferencesT>]
export type ReferencedDefinitions<ReferencesT extends UntypedReferenceList> = {
[Property in keyof ReferencedSchemaMap<ReferencesT>]?: Definition<ReferencedSchemaMap<ReferencesT>[Property]>
}
export type ReferencedSchema<ReferencesT extends AnyReferenceList> = ReferencedSchemaMap<ReferencesT>[keyof ReferencedSchemaMap<ReferencesT>]
export type ReferencedTypes<ReferencesT extends AnyReferenceList> = {
export type ReferencedTypes<ReferencesT extends UntypedReferenceList> = {
[Property in keyof ReferencedSchemaMap<ReferencesT>]?: Value<ReferencedSchemaMap<ReferencesT>[Property]>
}
// TODO: Add a test for a circular definition (i.e., an object that contains a reference to itself, for recursive objects like trees) in both one- and two-object loops
export function schema<
RepresentedT,
KeyT extends string,
ReferencesT extends AnySchemaData[],
ReferencesT extends SchemaData<any, any, any, ReferencedTypes<ReferencesT>>[],
>({schema, key, references}: { schema: JTDSchemaType<RepresentedT, ReferencedTypes<ReferencesT>>, key: KeyT, references: ReferencesT, typeHint?: RepresentedT|null }): SchemaData<RepresentedT, KeyT, ReferencesT, ReferencedTypes<ReferencesT>> {
const definition = {[key]: schema} as Definition<SchemaData<RepresentedT, KeyT, ReferencesT, ReferencedTypes<ReferencesT>>>
const reference = {ref: key}
const definitions = references.reduce<AnyDefinitions>((definitions, reference) => ({...reference.definition, ...definitions}), definition)
const validate = ajv.compile<RepresentedT>({ ...reference, definitions })
const definitions = references.reduce((definitions, reference) => ({...reference.definition, ...definitions}), {} as ReferencedDefinitions<ReferencesT>)
const validate = ajv.compile<RepresentedT>({ ...schema, definitions })
return {
schema,
key,

@ -1,236 +0,0 @@
import {DateTime} from "luxon";
export interface Serializer<DeserializedT, SerializedT> {
checkType(data: unknown): data is SerializedT
hydrate(data: SerializedT): DeserializedT
dehydrate(data: DeserializedT): SerializedT
}
export interface LocatedError {
readonly location: string
readonly error: Error|LocatedError
}
export function isLocatedError(object: unknown): object is LocatedError {
return typeof object === "object" && object !== null && object.hasOwnProperty("location") && object.hasOwnProperty("error")
}
export function wrapErrorWithLocation(location: string, object: unknown): LocatedError {
if (typeof object === "string") {
return {
location,
error: Error(object)
}
} else if (object instanceof Error || isLocatedError(object)) {
return {
location,
error: object
}
} else {
return {
location,
error: Error(`Unknown error: ${object}`)
}
}
}
export class SelfSerializer<T> implements Serializer<T, T> {
readonly checkType: (data: unknown) => data is T;
constructor(checkType: (data: unknown) => data is T) {
this.checkType = checkType
}
dehydrate(data: T): T {
return data;
}
hydrate(data: T): T {
return data;
}
}
export const StringSerializer = new SelfSerializer((data): data is string => typeof data === "string")
export const DateTimeSerializer: Serializer<DateTime, string> = {
checkType(data: unknown): data is string {
return typeof data === "string"
},
dehydrate(data: DateTime): string {
return data.toISO();
},
hydrate(data: string): DateTime {
return DateTime.fromISO(data);
}
}
export class EnumSerializer<T extends string|number> implements Serializer<T, T> {
readonly values: readonly T[]
constructor(values: readonly T[]) {
this.values = values
}
checkType(data: unknown): data is T {
if (!(this.values as unknown[]).includes(data)) {
throw Error("not a valid value for the enum")
}
return true
}
dehydrate(data: T): T {
return data;
}
hydrate(data: T): T {
return data;
}
}
export class NullableSerializer<DeserializedT, SerializedT> implements Serializer<DeserializedT | null, SerializedT | null> {
readonly inner: Serializer<DeserializedT, SerializedT>
constructor(inner: Serializer<DeserializedT, SerializedT>) {
this.inner = inner
}
checkType(data: unknown): data is SerializedT | null {
return data === null || this.inner.checkType(data)
}
dehydrate(data: DeserializedT | null): SerializedT | null {
if (data === null) {
return null
}
return this.inner.dehydrate(data);
}
hydrate(data: SerializedT | null): DeserializedT | null {
if (data === null) {
return null
}
return this.inner.hydrate(data);
}
}
export class OptionalSerializer<DeserializedT, SerializedT> implements Serializer<DeserializedT | undefined, SerializedT | undefined> {
readonly inner: Serializer<DeserializedT, SerializedT>
constructor(inner: Serializer<DeserializedT, SerializedT>) {
this.inner = inner
}
checkType(data: unknown): data is SerializedT | undefined {
return typeof data === "undefined" || this.inner.checkType(data)
}
dehydrate(data: DeserializedT | undefined): SerializedT | undefined {
if (typeof data === "undefined") {
return data
}
return this.inner.dehydrate(data);
}
hydrate(data: SerializedT | undefined): DeserializedT | undefined {
if (typeof data === "undefined") {
return data
}
return this.inner.hydrate(data);
}
}
export const OptionalStringSerializer = new OptionalSerializer(StringSerializer)
export const OptionalDateTimeSerializer = new OptionalSerializer(DateTimeSerializer)
export type KeySerializers<DeserializedT extends {[key in KeysT]?: unknown}, SerializedT extends {[key in KeysT]?: unknown}, KeysT extends string|number> = {
[key in KeysT]: Serializer<DeserializedT[key], SerializedT[key]>
}
export class ObjectSerializer<DeserializedT extends {[key in KeysT]?: unknown}, SerializedT extends {[key in KeysT]?: unknown}, KeysT extends string|number> implements Serializer<DeserializedT, SerializedT> {
readonly inner: KeySerializers<DeserializedT, SerializedT, KeysT>
readonly keys: KeysT[]
constructor(inner: KeySerializers<DeserializedT, SerializedT, KeysT>, keys: KeysT[]) {
this.inner = inner
this.keys = keys
}
checkType(data: unknown): data is SerializedT {
if (typeof data !== "object" || data === null) {
throw Error("expected an object")
}
const map: {[key in KeysT]?: unknown} = data
for (const key of this.keys) {
try {
if (!this.inner[key].checkType(map[key])) {
// noinspection ExceptionCaughtLocallyJS
throw Error("checkType returned false")
}
} catch (e) {
throw wrapErrorWithLocation(`at key ${key}`, e)
}
}
return true
}
dehydrate(data: DeserializedT): SerializedT {
const result: Partial<SerializedT> = {}
for (const key of this.keys) {
try {
result[key] = this.inner[key].dehydrate(data[key])
} catch (e) {
throw wrapErrorWithLocation(`at key ${key}`, e)
}
}
return result as SerializedT
}
hydrate(data: SerializedT): DeserializedT {
const result: Partial<DeserializedT> = {}
for (const key of this.keys) {
try {
result[key] = this.inner[key].hydrate(data[key])
} catch (e) {
throw wrapErrorWithLocation(`at key ${key}`, e)
}
}
return result as DeserializedT
}
}
export class ArraySerializer<DeserializedT, SerializedT> implements Serializer<DeserializedT[], SerializedT[]> {
readonly inner: Serializer<DeserializedT, SerializedT>
constructor(inner: Serializer<DeserializedT, SerializedT>) {
this.inner = inner
}
checkType(data: unknown): data is SerializedT[] {
return Array.isArray(data) && data.every((value, index) => {
try {
if (!this.inner.checkType(value)) {
// noinspection ExceptionCaughtLocallyJS
throw Error("checkType returned false")
}
} catch (e) {
throw wrapErrorWithLocation(`at index ${index}`, e)
}
})
}
dehydrate(data: DeserializedT[]): SerializedT[] {
return data.map((value, index) => {
try {
return this.inner.dehydrate(value)
} catch (e) {
throw wrapErrorWithLocation(`at index ${index}`, e)
}
});
}
hydrate(data: SerializedT[]): DeserializedT[] {
return data.map((value, index) => {
try {
return this.inner.hydrate(value)
} catch (e) {
throw wrapErrorWithLocation(`at index ${index}`, e)
}
});
}
}
export const ArrayStringSerializer = new ArraySerializer(StringSerializer)
export const OptionalArrayStringSerializer = new OptionalSerializer(ArrayStringSerializer)

@ -1,40 +1,14 @@
import {AnySchemaDataFor} from "./SchemaData.js";
import {SchemaData} from "./SchemaData";
import {dump, load} from "js-yaml";
import {InquireFunction, ShowFunction} from "../prompts/Inquire.js";
import {EditorQuestion, ExpandQuestion} from "inquirer";
import {EditorQuestionOptions} from "inquirer";
import {InquireFunction, ShowFunction} from "../prompts/Inquire";
const RETRY = "Retry"
const ABORT = "Abort"
export interface YamlPromptOptions<ObjectT> {
schema: AnySchemaDataFor<ObjectT>
currentValue: ObjectT|undefined
name: string
}
export interface YamlPromptSetOptions<ObjectT> extends YamlPromptOptions<ObjectT> {
currentValue: ObjectT
}
export interface YamlPromptDependencies {
inquire: InquireFunction<EditorQuestion, string> & InquireFunction<ExpandQuestion, typeof RETRY|typeof ABORT>
showError: ShowFunction
}
export type YamlPromptFunction = (<ObjectT>(opts: YamlPromptSetOptions<ObjectT>) => Promise<ObjectT>) & (<ObjectT>(opts: YamlPromptOptions<ObjectT>) => Promise<ObjectT|undefined>)
export function yamlPrompt(deps: YamlPromptDependencies): YamlPromptFunction {
function injectedYamlPrompt<ObjectT>(opts: YamlPromptSetOptions<ObjectT>): Promise<ObjectT>
function injectedYamlPrompt<ObjectT>(opts: YamlPromptOptions<ObjectT>): Promise<ObjectT|undefined>
function injectedYamlPrompt<ObjectT>(opts: YamlPromptOptions<ObjectT>): Promise<ObjectT|undefined> {
return promptForYaml<ObjectT>(opts, deps)
}
return injectedYamlPrompt
}
export async function promptForYaml<ObjectT>({schema, currentValue, name}: YamlPromptSetOptions<ObjectT>, {inquire, showError}: YamlPromptDependencies): Promise<ObjectT>
export async function promptForYaml<ObjectT>({schema, currentValue, name}: YamlPromptOptions<ObjectT>, {inquire, showError}: YamlPromptDependencies): Promise<ObjectT|undefined>
export async function promptForYaml<ObjectT>({schema, currentValue, name}: YamlPromptOptions<ObjectT>, {inquire, showError}: YamlPromptDependencies): Promise<ObjectT|undefined> {
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT>
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT|undefined, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT|undefined>
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT|undefined, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT|undefined> {
const original = dump(currentValue)
let text = original
while (true) {
@ -42,14 +16,14 @@ export async function promptForYaml<ObjectT>({schema, currentValue, name}: YamlP
type: "editor",
message: `View and edit ${name}:`,
default: text,
})
} as EditorQuestionOptions)
if (original !== modified) {
try {
const result = load(modified)
if (schema.validate(result)) {
return result
} else {
await showError(schema.validate.errors?.map((e) => e.toString()).join("; ") ?? "Failed validation")
await showError(schema.validate.errors?.map((e) => e.toString()).join("; ") || "Failed validation")
}
} catch (e) {
await showError(e.toString())
@ -58,8 +32,8 @@ export async function promptForYaml<ObjectT>({schema, currentValue, name}: YamlP
type: "expand",
message: "Choose how to proceed:",
choices: [
{key: "r", name: "Retry editing current text", value: RETRY},
{key: "a", name: "Abort editing and reset to original state", value: ABORT}
{key: "r", name: "Retry edits with current text", value: RETRY},
{key: "a", name: "Abort editing and keep original state", value: ABORT}
],
})
switch (option) {

@ -1,4 +1,4 @@
export function merge<Update extends {readonly [key: string]: unknown}, Data extends Update>(update: Update, base: Data): Data {
export function merge<Update extends {readonly [key: string]: any}, Data extends Update>(update: Update, base: Data): Data {
const data = {
...update,
...base,

@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "es6",
"module": "commonjs",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es6",

Loading…
Cancel
Save