Browse Source

cleaned up and added feature to browse other cards in member card

main
deicidus 2 years ago
parent
commit
7a9965052e
  1. 3
      .gitmodules
  2. 6
      index.js
  3. 11
      scripts/ao.js
  4. 1125
      scripts/api.js
  5. 39
      scripts/cards.js
  6. 4
      scripts/connect.js
  7. 5
      scripts/console.js
  8. 30
      scripts/crypto.js
  9. 2
      scripts/forest.js
  10. 178
      scripts/hand.js
  11. 104
      scripts/priority.js
  12. 2
      scripts/services.js
  13. 4
      scripts/session.js
  14. 120
      scripts/settings.js
  15. 11
      scripts/shadowchat.js
  16. 157
      scripts/tags.js
  17. 2
      scripts/tests.js
  18. 20
      scripts/util.js
  19. 11
      scripts/welcome.js
  20. 2
      scripts/wizard.js

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "ao-lib"]
path = ao-lib
url = http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-lib.git

6
index.js

@ -4,7 +4,7 @@ import { execSync } from 'child_process'
// Import ao-cli core features // Import ao-cli core features
import { exitIfRoot, detectOS, updateSoftware } from './scripts/system.js' import { exitIfRoot, detectOS, updateSoftware } from './scripts/system.js'
import { checkAoEnvFile, aoEnv, setAoEnv, AO_ENV_FILE_PATH } from './scripts/settings.js' import { checkAoEnvFile, aoEnv, setAoEnv, AO_ENV_FILE_PATH } from './ao-lib/settings.js'
import { unicornPortal, asciiArt, clearScreen } from './scripts/console.js' import { unicornPortal, asciiArt, clearScreen } from './scripts/console.js'
import { welcome, exclaim, farewell, yesOrNo, promptMenu } from './scripts/welcome.js' import { welcome, exclaim, farewell, yesOrNo, promptMenu } from './scripts/welcome.js'
import wander from './scripts/forest.js' import wander from './scripts/forest.js'
@ -12,7 +12,7 @@ import useAoMenu from './scripts/ao.js'
import aoInstallWizard, { chooseAoVersion, checkAo } from './scripts/wizard.js' import aoInstallWizard, { chooseAoVersion, checkAo } from './scripts/wizard.js'
import testsMenu from './scripts/tests.js' import testsMenu from './scripts/tests.js'
import { headerStyle } from './scripts/styles.js' import { headerStyle } from './scripts/styles.js'
import { sleep, randomInt } from './scripts/util.js' import { randomInt } from './ao-lib/util.js'
import './scripts/strings.js' import './scripts/strings.js'
// Import AO modular features // Import AO modular features
@ -20,6 +20,8 @@ import features, { featuresMenu } from './scripts/features/index.js'
import manual, { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './scripts/features/manual.js' import manual, { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './scripts/features/manual.js'
import aoCli from './scripts/features/ao-cli.js' import aoCli from './scripts/features/ao-cli.js'
const sleep = (ms = 550) => { return new Promise((r) => setTimeout(r, ms)) }
// Prints the AO Main Menu and executes the user's choice // Prints the AO Main Menu and executes the user's choice
async function mainMenu() { async function mainMenu() {
//console.log(`\n${headerStyle('AO Main Menu')}\n`) //console.log(`\n${headerStyle('AO Main Menu')}\n`)

11
scripts/ao.js

@ -2,10 +2,10 @@
// The ao-cli client has no store so it makes frequent (hopefully very quick) calls to get exactly the information it needs. // The ao-cli client has no store so it makes frequent (hopefully very quick) calls to get exactly the information it needs.
// This requires us to make sure the AO API server's REST API is concise and efficient. // This requires us to make sure the AO API server's REST API is concise and efficient.
// The only places ao-cli should call out to an AO server (i.e., use the API in api.js) are in this Use AO menu and in the Tests menu. // The only places ao-cli should call out to an AO server (i.e., use the API in api.js) are in this Use AO menu and in the Tests menu.
import { aoEnv } from './settings.js' import { aoEnv } from '../ao-lib/settings.js'
import { isLoggedIn, loginPrompt, logout } from './session.js' import { isLoggedIn, loginPrompt, logout } from './session.js'
import { getTopPriorityText } from './priority.js' import { getTopPriorityText } from './priority.js'
import { AO_DEFAULT_HOSTNAME } from './api.js' import { AO_DEFAULT_HOSTNAME } from '../ao-lib/api.js'
import { headerStyle } from './styles.js' import { headerStyle } from './styles.js'
import { cardMenu } from './cards.js' import { cardMenu } from './cards.js'
import { connectMenu } from './connect.js' import { connectMenu } from './connect.js'
@ -15,7 +15,6 @@ import { promptMenu } from './welcome.js'
// Prints the Use AO Menu and executes the user's choice. Using the AO as a client occurs only under this menu item (except Tests menu). // Prints the Use AO Menu and executes the user's choice. Using the AO as a client occurs only under this menu item (except Tests menu).
export default async function useAoMenu() { export default async function useAoMenu() {
const loggedIn = isLoggedIn() const loggedIn = isLoggedIn()
console.log(`\n${headerStyle('AO')}\n`)
if(loggedIn) { if(loggedIn) {
console.log('Logged in as:', aoEnv('AO_CLI_SESSION_USERNAME')) console.log('Logged in as:', aoEnv('AO_CLI_SESSION_USERNAME'))
const topPriority = await getTopPriorityText() const topPriority = await getTopPriorityText()
@ -30,20 +29,20 @@ export default async function useAoMenu() {
aoMenuChoices.push( aoMenuChoices.push(
'Deck', 'Deck',
'Chat', 'Chat',
'Connect', 'P2P',
) )
} }
aoMenuChoices.push( aoMenuChoices.push(
loggedIn ? 'Log Out' : 'Log In', loggedIn ? 'Log Out' : 'Log In',
'Back to Main Menu' 'Back to Main Menu'
) )
const answer = await promptMenu(aoMenuChoices, 'Use AO features', 'connect to an AO server to use AO features') const answer = await promptMenu(aoMenuChoices, 'AO', 'connect to an AO server to use AO features')
switch(answer) { switch(answer) {
case 'Deck': case 'Deck':
//await todoList('My Todo List', ['Add full AO install process to ao-cli in convenient format', 'Add AO server unit tests to ao-cli', 'Get groceries', 'Play music every day']) //await todoList('My Todo List', ['Add full AO install process to ao-cli in convenient format', 'Add AO server unit tests to ao-cli', 'Get groceries', 'Play music every day'])
while(await cardMenu()) {} while(await cardMenu()) {}
break break
case 'Connect': case 'P2P':
while(await connectMenu()) {} while(await connectMenu()) {}
break break
case 'Chat': case 'Chat':

1125
scripts/api.js

File diff suppressed because it is too large Load Diff

39
scripts/cards.js

@ -1,25 +1,34 @@
// Cards module - everything related to cards should go here (database install is automatic for AO server so no feature module) // Cards module - everything related to cards should go here (database install is automatic for AO server so no feature module)
import { getCardByName, createCard, prioritizeCard } from './api.js' import { getCardByName, createCard, prioritizeCard } from '../ao-lib/api.js'
import { headerStyle } from './styles.js' import { headerStyle } from './styles.js'
import { prioritiesMenu } from './priority.js' import { prioritiesMenu } from './priority.js'
import { aoEnv } from './settings.js' import { subcardsMenu } from './hand.js'
import { aoEnv } from '../ao-lib/settings.js'
import { promptMenu, askQuestionText } from './welcome.js' import { promptMenu, askQuestionText } from './welcome.js'
import { tagsMenu } from './tags.js'
// The card menu is complex and so has been split into this separate file // The card menu is complex and so has been split into this separate file
export async function cardMenu() { export async function cardMenu() {
const cardChoices = [ const cardChoices = [
{ title: 'Top priorities', value: 'priorities', short: 'priorities' }, // hand? (7) (add #s in parens) { title: 'Top priorities', value: 'priorities', short: 'priorities' }, // hand? (7) (add #s in parens)
{ title: 'Cards in hand', value: 'subcards', short: 'hand' }, // (current) deck? (60) { title: 'Cards in hand', value: 'subcards', short: 'hand' }, // (current) deck? (60)
{ title: 'Browse full deck', value: 'browse', short: 'browse' }, // archive? (10,000) { title: 'Tags', value: 'tags', short: 'tags' }, // magically abstracting hierarchical nametags
//{ title: 'Browse full deck', value: 'search', short: 'browse' }, // archive? (10,000) // will become search-as-you-type for card
'Back to AO Menu' 'Back to AO Menu'
] ]
const answer = await promptMenu(cardChoices, 'My Deck') const answer = await promptMenu(cardChoices, 'My Deck')
switch(answer.card_menu) { switch(answer) {
case 'priorities': case 'priorities':
while(await prioritiesMenu()) {} while(await prioritiesMenu()) {}
break break
case 'subcards': case 'subcards':
while(await subcardsMenu()) {} let previousAnswer
do {
previousAnswer = await subcardsMenu(undefined, previousAnswer) // undefined taskId defaults to member card
} while(previousAnswer !== false) {}
break
case 'tags':
while(await tagsMenu()) {}
break break
case 'browse': case 'browse':
while(await browseMenu()) {} while(await browseMenu()) {}
@ -30,27 +39,23 @@ export async function cardMenu() {
return true return true
} }
async function subcardsMenu() {
console.log('Not yet implemented')
}
async function browseMenu() { async function browseMenu() {
console.log('Not yet implemented') console.log('Not yet implemented')
} }
// Ask the user to create a card, checks if it already exists, and then creates it if it doesn't // Ask the user to create a card, checks if it already exists, and then creates it if it doesn't
export async function createCardInteractive(prioritized = true) { export async function createCardInteractive(prioritized = false) {
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID') const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
if(!memberId) { if(!memberId) {
console.log('Not logged in.') console.log('Not logged in.')
return false return false
} }
const answer = await askQuestionText('New card or Enter to end:') const answer = await askQuestionText('New card or Enter to end:')
if(answer.new_card_text.trim().length <= 0) { if(answer === false || answer.trim().length <= 0) {
return false return false
} }
// Check if the card alerady exists // Check if the card alerady exists
const fetchedCards = await getCardByName(answer.new_card_text, false) const fetchedCards = await getCardByName(answer, false)
if(fetchedCards && fetchedCards.length >= 1) { if(fetchedCards && fetchedCards.length >= 1) {
if(fetchedCards.length >= 2) { if(fetchedCards.length >= 2) {
console.log('More than one copy of this card was found. This should not happen.') console.log('More than one copy of this card was found. This should not happen.')
@ -61,10 +66,12 @@ export async function createCardInteractive(prioritized = true) {
if(!prioritizeResult.ok) { if(!prioritizeResult.ok) {
console.log('May have failed to prioritize card:', prioritizeResult) console.log('May have failed to prioritize card:', prioritizeResult)
} }
} else {
// todo: subtask the card
} }
return false return answer
} }
console.log('card does not exist yet. creating...', answer.new_card_text) console.log('card does not exist yet. creating...', answer)
const result = await createCard(answer.new_card_text, false, true) const result = await createCard(answer, false, prioritized)
return answer.new_card_text return answer
} }

4
scripts/connect.js

@ -1,9 +1,9 @@
// Each AO API server can connect peer-to-peer over Tor. Tor addresses are unique and data is end-to-end encrypted. // Each AO API server can connect peer-to-peer over Tor. Tor addresses are unique and data is end-to-end encrypted.
import { headerStyle } from './styles.js' import { headerStyle } from './styles.js'
import { aoEnv, setAoEnv } from './settings.js' import { aoEnv, setAoEnv } from '../ao-lib/settings.js'
import { isLoggedIn } from './session.js' import { isLoggedIn } from './session.js'
import { isInstalled } from './features/tor.js' import { isInstalled } from './features/tor.js'
import { connectToAo, getAoBootstrapList, bootstrap } from './api.js' import { connectToAo, getAoBootstrapList, bootstrap } from '../ao-lib/api.js'
import { roger, promptMenu } from './welcome.js' import { roger, promptMenu } from './welcome.js'
// Prints a menu to connect your AO to other AOs and manage connections // Prints a menu to connect your AO to other AOs and manage connections

5
scripts/console.js

@ -3,8 +3,7 @@ import chalkAnimation from 'chalk-animation'
import gradient from 'gradient-string' import gradient from 'gradient-string'
import figlet from 'figlet' import figlet from 'figlet'
import { createSpinner } from 'nanospinner' import { createSpinner } from 'nanospinner'
import { sleep } from './util.js' import { delay as sleep, selectRandom } from '../ao-lib/util.js'
import { selectRandom } from './util.js'
import { centerLines } from './strings.js' import { centerLines } from './strings.js'
// Displays a brief randomly-selected rainbow-animated phrase // Displays a brief randomly-selected rainbow-animated phrase
@ -43,4 +42,4 @@ export function spinner(waitingMessage = 'Please wait...', doneMessage = 'Done.'
return (doneMessageOverwrite = null) => { return (doneMessageOverwrite = null) => {
spinner.success({text: doneMessageOverwrite || doneMessage}) spinner.success({text: doneMessageOverwrite || doneMessage})
} }
} }

30
scripts/crypto.js

@ -1,30 +0,0 @@
import crypto from 'crypto' // Does not work on client because this is a Node library, but works for below server-only functions
// These libraries are old but they work and can be included on both server and client
import shajs from 'sha.js'
import hmac from 'hash.js/lib/hash/hmac.js'
import sha256 from 'hash.js/lib/hash/sha/256.js' // Only works for shorter hashes, not in createHash used for hashing meme files
export function createHash(payload) {
return shajs('sha256').update(payload).digest('hex')
}
export function hmacHex(data, signingKey) {
return hmac(sha256, signingKey).update(data).digest('hex')
}
export function derivePublicKey(p) {
return crypto.createPublicKey(p).export({
type: 'spki',
format: 'pem',
})
}
export function encryptToPublic(pub, info) {
return crypto.publicEncrypt(pub, new Buffer(info)).toString('hex')
}
export function decryptFromPrivate(priv, hiddenInfo) {
return crypto
.privateDecrypt(priv, Buffer.from(hiddenInfo, 'hex'))
.toString('latin1')
}

2
scripts/forest.js

@ -1,5 +1,5 @@
// Greeting text functions that can be hooked into your cd function so that moving between folders becomes an experience of the AO // Greeting text functions that can be hooked into your cd function so that moving between folders becomes an experience of the AO
import { selectRandom, randomInt } from './util.js' import { selectRandom, randomInt } from '../ao-lib/util.js'
import { isLoggedIn } from './session.js' import { isLoggedIn } from './session.js'
import { getTopPriorityText } from './priority.js' import { getTopPriorityText } from './priority.js'

178
scripts/hand.js

@ -0,0 +1,178 @@
// View and create cards with the .subtask of other cards, which is an array of taskIds
import { headerStyle } from './styles.js'
import { aoEnv } from '../ao-lib/settings.js'
import { getCard, playCard, completeCard, uncheckCard, discardCardFromCard, prioritizeCard, grabCard, dropCard } from '../ao-lib/api.js'
import { getNewHighestEchelonScore } from './priority.js'
import { createCardInteractive } from './cards.js'
import { promptMenu } from './welcome.js'
import { tagCardInteractive, viewTagMenu } from './tags.js'
// Displays the subtasks of the given taskId in a menu. Selecting a card shows a menu for that card. If taskId is null, member card is used.
// The terms tasks and cards are used mostly interchangeably in the code. For the user, 'subcards' is preferred for clarity/generality,
// but can be used to refer either to the .subTasks or all of the cards in another card (including subTasks, priorities, completed, and pinned)
export async function subcardsMenu(taskId = null, previousIndex = null) {
console.log(`\n${headerStyle('Cards in My Hand')}`)
let subtaskChoices = []
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
if(!memberId) {
console.log('Not logged in.')
return false
}
if(!taskId) {
// Get the subtasks of my member card
taskId = memberId
}
const fetchedCards = await getCard(taskId, 'subcards') // will fetch both priorities and subtasks in one array
if(!fetchedCards || fetchedCards.length < 1) {
console.log('Failed to fetch the specified card, this is bad.')
return false
}
const card = fetchedCards[0] // first card is the requested card itself
// Separate fetched cards into correct ordered list of priorities and subtasks
let priorityCards = card.priorities.map((priorityTaskId, i) => {
const priorityCard = fetchedCards.find(p => p.taskId === priorityTaskId)
if(!priorityCard) {
return 'Missing card, repair your database'
}
return priorityCard
})
priorityCards.reverse()
const subtaskCards = card.subTasks.map((subtaskTaskId, i) => {
const subtaskCard = fetchedCards.find(st => st.taskId === subtaskTaskId)
if(!subtaskCard) {
return 'Missing card, repair your database'
}
return subtaskCard
})
console.log('There are', subtaskCards.length, 'subcards in this card')
subtaskChoices = subtaskCards.map((subtaskCard, i) => {
const shortenedName = subtaskCard.name.substring(0, 70) + (subtaskCard.name.length >= 70 ? '...' : '')
return {
title: shortenedName,
value: { index: i, card: subtaskCard },
short: shortenedName
}
})
subtaskChoices.push(
{ title: 'Play card here', value: 'create_here', short: 'new card' },
{ title: 'Back Up', value: false, short: 'back' }
)
const cardName = taskId === memberId ? 'My Hand' : card.name.substring(0, 70).toTitleCase()
const answer = await promptMenu(subtaskChoices, 'Cards in ' + cardName, undefined, previousIndex)
switch(answer) {
case false:
return false
case 'create_here':
let previousCardCreatedText
do {
previousCardCreatedText = await createCardInteractive()
console.log('done with round')
} while(previousCardCreatedText != '\n')
console.log('returning true')
return answer.index
case 'Missing card, repair your database':
console.log('Database repair yet implemented, sorry.')
return answer.index
}
if(answer === false) {
return previousIndex
}
let chosenTask = answer.card
const chosenTaskId = chosenTask.taskId
let previousAnswer
do {
previousAnswer = await subtaskCardMenu(chosenTask, answer.index, taskId, priorityCards) // send priorities for echelon info in case they upboat
if(previousAnswer) {
const fetchedCards = await getCard(chosenTaskId, false)
if(!fetchedCards || fetchedCards.length < 1) {
console.log('The card has disappeared. Maybe it was deleted, or cards held by no one are automatically cleaned up every five minutes.')
return false
}
chosenTask = fetchedCards[0]
}
} while(previousAnswer !== false)
return answer.index
}
// Short action-oriented menu for cards in the subtasks list
// Index is the position of the card in the list that it is in, used for fencepost case to display upboat contextually
// inId is the taskId of the parent card that we are in contextually as we look at the given card in its list
// allPriorities is an array of task objects for the other priorities in the .priorities for the card this card is in (adjacent to this card)
async function subtaskCardMenu(card, index, inId, allPriorities) {
if(!card) {
console.log('subtaskCardMenu: card is required.')
return false
}
const taskId = card.taskId
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
if(!memberId) {
console.log('Not logged in.')
return false
}
const isChecked = card.claimed.includes(memberId)
const guild = card.guild === true ? card.name.toTitleCase() : card.guild || false
let subtaskChoices = []
const isInDeck = card.deck.includes(memberId)
if(!isInDeck) {
subtaskChoices.push({ title: 'Grab card (add to my deck)', value: 'grab', short: 'grab card' })
}
subtaskChoices.push(
{ title: 'Discard from hand (downboat)', value: 'downboat', short: 'downboat' },
{ title: 'Prioritize (upboat)', value: 'upboat', short: 'upboat' },
{ title: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' },
{ title: guild ? 'Tag: ' + guild : 'Tag', value: 'tag', short: 'tag' }
)
if(guild) {
subtaskChoices.push({ title: 'View tag', value: 'view_tag', short: 'view tag' })
}
if(isInDeck) {
subtaskChoices.push({ title: 'Remove from my deck', value: 'drop', short: 'drop card' })
}
subtaskChoices.push(
{ title: 'Browse within', value: 'browse', short: 'browse' },
{ title: 'Back Up', value: false, short: 'back' }
)
const answer = await promptMenu(subtaskChoices, 'Card: ' + card.name)
switch(answer) {
case 'grab':
await grabCard(taskId)
break
case 'drop':
await dropCard(taskId)
break
case 'check':
if(isChecked) {
await uncheckCard(taskId)
} else {
await completeCard(taskId)
}
break
case 'tag':
await tagCardInteractive(card)
break
case 'view_tag':
while(await viewTagMenu(card.guild))
break
case 'upboat':
console.log('upboat')
const { newPosition, newEchelonScore } = getNewHighestEchelonScore(card.echelon, allPriorities)
//console.log('newPosition is', newPosition, 'and newEchelonScore is', newEchelonScore)
await prioritizeCard(taskId, inId, newPosition, newEchelonScore)
return false
case 'downboat':
console.log(taskId, inId, 'discard')
await discardCardFromCard(taskId, inId)
return false
case 'browse':
let previousAnswer
do {
previousAnswer = await subcardsMenu(taskId, previousAnswer) // undefined taskId defaults to member card
} while(previousAnswer !== false) {}
break
break
default:
return false
}
return true
}

104
scripts/priority.js

@ -1,7 +1,7 @@
// Prioritize cards within other cards. Each card has a .priorities array of other taskIds. // Prioritize cards within other cards. Each card has a .priorities array of other taskIds.
import { headerStyle } from './styles.js' import { headerStyle } from './styles.js'
import { aoEnv } from './settings.js' import { aoEnv } from '../ao-lib/settings.js'
import { getCard, prioritizeCard, completeCard, uncheckCard, refocusCard } from './api.js' import { getCard, prioritizeCard, completeCard, uncheckCard, refocusCard } from '../ao-lib/api.js'
import { createCardInteractive } from './cards.js' import { createCardInteractive } from './cards.js'
import { promptMenu } from './welcome.js' import { promptMenu } from './welcome.js'
@ -64,10 +64,11 @@ export async function prioritiesMenu(taskId = null) {
if(!priorityCard) { if(!priorityCard) {
return 'Missing card, repair your database' return 'Missing card, repair your database'
} }
const shortenedName = priorityCard.name.substring(0, 70) + (priorityCard.name.length >= 70 ? '...' : '')
return { return {
title: priorityCard.name, title: shortenedName,
value: { index: i, card: priorityCard }, value: { index: i, card: priorityCard },
short: priorityCard.name.substring(0, 70) + priorityCard.name.length >= 70 ? '...' : '' short: shortenedName
} }
}) })
let firstIndexEchelonDecreases let firstIndexEchelonDecreases
@ -84,24 +85,24 @@ export async function prioritiesMenu(taskId = null) {
} }
} }
prioritiesChoices.push( prioritiesChoices.push(
{ title: 'Create priority', value: 'create_here', short: 'new priority' }, { title: 'Play priority', value: 'create_here', short: 'new priority' },
{ title: 'Back to Deck', value: false, short: 'back' } { title: 'Back to Deck', value: false, short: 'back' }
) )
const answer = await promptMenu(prioritiesChoices, 'My Priorities', undefined, undefined, 'Upboated tasks will be inserted above this line') const answer = await promptMenu(prioritiesChoices, 'My Priorities', undefined, undefined, 'Prioritized cards will be inserted above this line')
switch(answer) { switch(answer) {
case false: case false:
return false return false
case 'create_here': case 'create_here':
let previousCardCreatedText let previousCardCreatedText
do { do {
previousCardCreatedText = await createCardInteractive() previousCardCreatedText = await createCardInteractive(true)
} while(previousCardCreatedText != '\n') } while(previousCardCreatedText != '\n')
return true return true
case 'Missing card, repair your database': case 'Missing card, repair your database':
console.log('Database repair yet implemented, sorry.') console.log('Database repair yet implemented, sorry.')
return true return true
} }
let chosenTask = answer.priorities_menu.card let chosenTask = answer.card
const chosenTaskId = chosenTask.taskId const chosenTaskId = chosenTask.taskId
let previousAnswer let previousAnswer
do { do {
@ -115,12 +116,12 @@ export async function prioritiesMenu(taskId = null) {
chosenTask = fetchedCards[0] chosenTask = fetchedCards[0]
} }
} while(previousAnswer !== false) } while(previousAnswer !== false)
console.log('Card menu not yet implemented.')
return true return true
} }
// Short action-oriented menu for cards in the priorities list // Short action-oriented menu for cards in the priorities list
// Index is the position of the card in the list that it is in, used for fencepost case to display upboat contextually // Index is the position of the card in the list that it is in, used for fencepost case to display upboat contextually
// allPriorities is an array of task objects for the other priorities in the priorities list this card is in (adjacent to this card)
async function priorityCardMenu(card, index, allPriorities) { async function priorityCardMenu(card, index, allPriorities) {
if(!card) { if(!card) {
console.log('priorityCardMenu: card is required.') console.log('priorityCardMenu: card is required.')
@ -135,11 +136,11 @@ async function priorityCardMenu(card, index, allPriorities) {
const isChecked = card.claimed.includes(memberId) const isChecked = card.claimed.includes(memberId)
let priorityChoices = [] let priorityChoices = []
if(index != 0) { if(index != 0) {
priorityChoices.push({ title: 'Upboat', value: 'upboat', short: 'upboat' }) priorityChoices.push({ title: 'Prioritize (upboat)', value: 'upboat', short: 'upboat' })
} }
priorityChoices.push( priorityChoices.push(
{ title: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' }, { title: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' },
{ title: 'Downboat', value: 'downboat', short: 'downboat' }, { title: 'Discard from priorities (downboat)', value: 'downboat', short: 'downboat' },
//{ title: 'Browse within', value: 'browse', short: 'browse' } //{ title: 'Browse within', value: 'browse', short: 'browse' }
{ title: 'Back to Priorities', value: false, short: 'back' } { title: 'Back to Priorities', value: false, short: 'back' }
) )
@ -153,48 +154,7 @@ async function priorityCardMenu(card, index, allPriorities) {
} }
break break
case 'upboat': case 'upboat':
console.log('upboat') const { newPosition, newEchelonScore } = getNewHighestEchelonScore(card.echelon, allPriorities)
let firstEchelonScore
let newEchelonScore
let newPosition = 0
console.log('upboat2')
console.log('card is', card)
//console.log(allPriorities)
breakHere:
for(let i = 0; i < allPriorities.length; i++) {
console.log('upboat3')
const priority = allPriorities[i]
console.log('priority is', priority)
if(i === 0) {
console.log('upboat3.1')
firstEchelonScore = priority.echelon
console.log('upboat3.11115', priority.name, priority.echelon, typeof priority.echelon)
if(isNaN(firstEchelonScore)) {
console.log('upboat3.2')
newEchelonScore = 1
break breakHere
}
if(!card.echelon || card.echelon < priority.echelon) {
console.log('upboat3.3')
newEchelonScore = priority.echelon
} else if(card.echelon && priority.echelon && card.echelon === priority.echelon) {
console.log('upboat3.4')
newEchelonScore = priority.echelon + 1
break breakHere
} else if(card.echelon && priority.echelon && card.echelon > priority.echelon) {
console.log('upboat3.5')
break breakHere
}
}
console.log('upboat4')
if(priority.echelon !== firstEchelonScore) {
newPosition = i
break breakHere
}
console.log('upboat5')
}
console.log('upboat6')
console.log('newPosition is', newPosition, 'and newEchelonScore is', newEchelonScore)
await prioritizeCard(taskId, memberId, newPosition, newEchelonScore) await prioritizeCard(taskId, memberId, newPosition, newEchelonScore)
return false return false
case 'downboat': case 'downboat':
@ -207,3 +167,41 @@ async function priorityCardMenu(card, index, allPriorities) {
} }
return true return true
} }
// Calculates and returns what the echelon score should be to prioritize a priority with the given echelon score to the correct place
// in tnhe given list of tasks.
// newPriorityEchelonScore is the .echolon of the task object about to be prioritized
// allPriorities is an array of task objects for the other priorities in the priorities list this card is in (adjacent to this card)
export function getNewHighestEchelonScore(newPriorityEchelon, allPriorities) {
let firstEchelonScore
let newEchelonScore
let newPosition = 0
for(let i = 0; i < allPriorities.length; i++) {
const priority = allPriorities[i]
if(i === 0) {
firstEchelonScore = priority.echelon
if(isNaN(firstEchelonScore)) {
// Top priority does have a (valid) echelon score, so echelon = 0 and the new echelon score should be 1
newEchelonScore = 1
break
}
if(!newPriorityEchelon || newPriorityEchelon < priority.echelon) {
// Prioritized card does not have an echelon score or it is less than the current priority's echelon score (so keep going to find insert position)
newEchelonScore = priority.echelon
//break
} else if(newPriorityEchelon && priority.echelon && newPriorityEchelon === priority.echelon) {
// Echelon of new priority is same as top priority, so increase it by one (and it goes to top)
newEchelonScore = priority.echelon + 1
break
} else if(newPriorityEchelon && priority.echelon && newPriorityEchelon > priority.echelon) {
// Echelon of new priority is already greater than the new priority, so its echelon doesn't change (and it goes to top)
break
}
}
if(priority.echelon !== firstEchelonScore) {
newPosition = i
break
}
}
return { newPosition, newEchelonScore }
}

2
scripts/services.js

@ -2,7 +2,7 @@
import { execSync } from 'child_process' import { execSync } from 'child_process'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { aoEnv, setAoEnv } from './settings.js' import { aoEnv, setAoEnv } from '../ao-lib/settings.js'
import { isFile } from './files.js' import { isFile } from './files.js'
import { askQuestionText } from './welcome.js' import { askQuestionText } from './welcome.js'

4
scripts/session.js

@ -1,5 +1,5 @@
import { createSession, logout as apiLogout } from './api.js' import { createSession, logout as apiLogout } from '../ao-lib/api.js'
import { aoEnv, setAoEnv } from './settings.js' import { aoEnv, setAoEnv } from '../ao-lib/settings.js'
import { askQuestionText } from './welcome.js' import { askQuestionText } from './welcome.js'
// Returns true if there is a session cookie for ao-cli saved in the AO .env file (=ready to make session requests) // Returns true if there is a session cookie for ao-cli saved in the AO .env file (=ready to make session requests)

120
scripts/settings.js

@ -1,120 +0,0 @@
import { execSync } from 'child_process'
import fs from 'fs'
import { parse, stringify } from 'envfile'
export const AO_ENV_FILE_PATH = process.env.HOME + '/.ao/.env'
function createAoFolderIfDoesNotExist() {
}
// Check for an AO env file at ~/.ao/.env and returns true if it exists
export function checkAoEnvFile() {
try {
execSync(`[ -f "${AO_ENV_FILE_PATH}" ]`)
return true
} catch(err) {
return false
}
}
export function aoEnv(variable) {
let envFileContents = {}
try {
envFileContents = fs.readFileSync(AO_ENV_FILE_PATH)
} catch(err) {
if(err.code === 'ENOENT') {
//console.log('The .env file does not exist, so the requested value', variable, 'is empty.')
} else {
console.log('Unknown error loading .env file in aoEnv, aborting.')
}
return null
}
const parsedFile = parse(envFileContents)
if(!parsedFile.hasOwnProperty(variable)) {
return null
}
// Convert ENV idiom to programmatic types
switch(parsedFile[variable]) {
case '1':
case 'true':
case 'TRUE':
case 'yes':
case 'YES':
return true
case '0':
case 'false':
case 'FALSE':
case 'no':
case 'NO':
return false
}
return parsedFile[variable]
}
// Sets and saves the given ENV=value to the global ~/.ao/.env file
// If value is null, the env variable will be deleted
// Returns true if a change was made, false if no change was made or if it failed
export function setAoEnv(variable, value) {
createAoFolderIfDoesNotExist()
if(typeof variable !== 'string') {
console.log('ENV variable name must be a string for setAoEnv')
return false
}
// Convert types to standard ENV file idiom
switch(value) {
case true:
case 'TRUE':
case 'yes':
case 'YES':
value = '1'
break
case false:
case 'FALSE':
case 'no':
case 'NO':
value = '0'
}
let envFileContents = {}
try {
envFileContents = fs.readFileSync(AO_ENV_FILE_PATH)
} catch(err) {
if(err.code === 'ENOENT') {
console.log('The .env file hasn\'t been created yet, creating.')
} else {
console.log('Unknown error loading .env file in setAoEnv, aborting. Error:', err)
return false
}
}
const parsedFile = parse(envFileContents)
if(parsedFile[variable] == value) {
console.log(variable, 'is already', value, 'so no change was made.')
return false
}
if(value === null) {
delete parsedFile[variable]
} else {
parsedFile[variable] = value
}
const stringified = stringify(parsedFile)
fs.writeFileSync(AO_ENV_FILE_PATH, stringified)
// Confirm the variable was set in the .env file correctly
if(aoEnv(variable) != value) {
console.log('Value was not saved correctly, sorry.')
return false
}
return true
}
function setAndSaveEnvironmentVariable(variable, value, path) {
}

11
scripts/shadowchat.js

@ -1,19 +1,21 @@
// AO shadowchat feature menu including bootstrap network server list browser, chatroom list on each server, and chatroom interface // AO shadowchat feature menu including bootstrap network server list browser, chatroom list on each server, and chatroom interface
// Called shadowchat because no record is kept of the chat messages, and all connections happen E2E over tor // Called shadowchat because no record is kept of the chat messages, and all connections happen E2E over tor
// As this feature gets build, sensible standards must be developed around when tor addresses change hands, when users authenticate, etc // As this feature gets build, sensible standards must be developed around when tor addresses change hands, when users authenticate, etc
import { aoEnv } from './settings.js' import { aoEnv } from '../ao-lib/settings.js'
import { isLoggedIn } from './session.js' import { isLoggedIn } from './session.js'
import { startPublicBootstrap } from './bootstrap.js' import { startPublicBootstrap } from './bootstrap.js'
import { headerStyle } from './styles.js' import { headerStyle } from './styles.js'
import { sleep } from './util.js'
import { askQuestionText, promptMenu } from './welcome.js' import { askQuestionText, promptMenu } from './welcome.js'
import { AO_DEFAULT_HOSTNAME, startSocketListeners, socketStatus, socket, shadowchat } from './api.js' import { AO_DEFAULT_HOSTNAME, startSocketListeners, socketStatus, socket, shadowchat } from '../ao-lib/api.js'
import { execSync } from 'child_process'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import path from 'path' import path from 'path'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
const sleep = (ms = 550) => { return new Promise((r) => setTimeout(r, ms)) }
// Prints a menu that allows you to join the global AO chatrooms // Prints a menu that allows you to join the global AO chatrooms
export default async function chatMenu() { export default async function chatMenu() {
let answers = {} let answers = {}
@ -64,7 +66,8 @@ export default async function chatMenu() {
while(await browseChatrooms()) {} while(await browseChatrooms()) {}
break break
case 'join_chat': case 'join_chat':
console.log('Not yet implemented') console.log('Launching Simplex Chat...')
execSync('bash simplex-chat')
break break
case 'Address Book': case 'Address Book':
console.log('The point of this address book is to make it possible to type short, one-word names and have them resolve to tor addresses.') console.log('The point of this address book is to make it possible to type short, one-word names and have them resolve to tor addresses.')

157
scripts/tags.js

@ -0,0 +1,157 @@
// Also called guilds, groups, or the title of a card, the .guild field of a card can hold a string that is 'about' the card, or true (and the tag is then the card .name itself)
// Right now one tag can be added to each card. Maybe it should be multiple, with tag #1 being the main/title tag
// When viewing a card, it should show the tag in the menu, or if not tagged, the option to tag it (with setTagInteractive)
// If the tag is set, an option should appear to view the tag (menu)
// When viewing a tag, it should be possible tag the tag. This creates the tag as a card with the new tag as its tag
// When viewing a tag, it should be possible to view all the cards tagged by that tag
// Because tags are implemented as part of cards, to put a tag in another tag, we make a card for that tag and tag it (give it a .guild)
import { askQuestionText, promptMenu } from './welcome.js'
import { tagCard, getCardsForTag, setCardProperty, getCardByName, createCard } from '../ao-lib/api.js'
// Tells the user the current guild/tag of the specified card, and allows them to set a new one (or blank to leave unchanged)
export async function tagCardInteractive(card) {
if(card.guild === true) {
} else if(card.guild) {
console.log('This card\'s tag is:')
console.log(card.guild)
} else {
console.log('This card is not yet tagged.')
}
const answer = await askQuestionText('Type new tag (or Enter/ESC to cancel):')
if(!answer || answer === 'ESC' || answer == '') {
return
}
await tagCard(card.taskId, answer)
}
// The tags index displays the tag of every card with a tag, sorted by echelon, then by we'll see!
export async function tagsMenu() {
const allTaggedCards = await getCardsForTag('*')
// Build a pure list of tags (since we don't do this in the database)
const tags = {}
allTaggedCards.forEach(card => {
const guild = card.guild === true ? card.name.toTitleCase() : card.guild
if(guild.includes('test')) console.log('guild:', guild, 'card is', card)
//console.log('guild is', guild)
// Skip any card that is within another tagged card or tagged with the name of another tag
if(allTaggedCards.some(t => {
if(t.taskId === card.taskId) return false
if(t.priorities.concat(t.subTasks, t.completed).includes(card.taskId)) {
if(guild.includes('test')) console.log('reason 1, t is', t)
return true
}
if(t.pins && t.pins.some(pin => pin.taskId === card.taskId)) {
if(guild.includes('test')) console.log('reason 2')
return true
}
if(card.taskId !== t.taskId && guild === card.name && card.guild && (card.echelon || 0) < (t.echelon || 0)) {
if(guild.includes('test')) console.log('reason 3')
return true
}
return false
})) {
return
}
if(!tags.hasOwnProperty(guild)) {
tags[guild] = card.echelon
return
}
if(!tags[guild] || tags[guild] < card.echelon) {
tags[guild] = card.echelon
}
})
let tagChoices = Object.entries(tags).sort((entryA, entryB) => {
return (entryB[1] || 0) - (entryA[1] || 0)
})
tagChoices = tagChoices.map((entry, i) => {
const [tag, echelon] = entry
const paddedEchelon = echelon >= 0 ? echelon.toString().padEnd(4) : ' '
return { title: paddedEchelon + tag, value: tag, short: 'view tag' }
})
tagChoices.push({ title: 'Back to Deck Menu', value: false, short: 'back' })
const answer = await promptMenu(tagChoices, 'All Tags')
console.log('answer:', answer)
if(!answer) {
return false
}
await viewTagMenu(answer, allTaggedCards)
return true
}
// Prints the menu for viewing a single tag, with options to view cards tagged by the tag, or to tag the tag
export async function viewTagMenu(tag, allTaggedCards) {
const tagChoices = [
{ title: 'Prioritize tag', value: 'upboat', short: 'prioritize tag' },
{ title: 'Cards tagged \'' + tag + '\'', value: 'tagged_cards', short: 'view tagged cards' }, // (current) deck? (60)
{ title: 'Tag this tag', value: 'tagtag' }, // hand? (7) (add #s in parens)
'Back to Tags Index'
]
const answer = await promptMenu(tagChoices, 'My Deck')
switch(answer) {
case 'upboat':
await tagToTopEchelon(tag, allTaggedCards)
return false
case 'tagged_cards':
while(await cardsTaggedByMenu(tag)) {}
break
case 'tagtag':
await tagTagInteractive(tag, allTaggedCards)
break
default:
return false
}
return true
}
// Finds the first card with the given tag and sets its echelon score to the highest + 1 of all tagged cards
async function tagToTopEchelon(tag, allTaggedCards) {
let highestEchelonScore = 0
allTaggedCards.forEach(card => {
if(card.echelon > highestEchelonScore) {
highestEchelonScore = card.echelon
}
})
highestEchelonScore++
const firstCardWithTag = allTaggedCards.find(card => card.guild === tag || (card.guild === true && card.name.toTitleCase() === tag))
if(!firstCardWithTag) {
console.log('Could not find the tag you just selected, this is bad')
return
}
await setCardProperty(firstCardWithTag.taskId, 'echelon', highestEchelonScore)
return
}
// Prints an interactive list of all cards tagged by the given tag, that can be browsed just like in the priorities and deck browsers
async function cardsTaggedByMenu(tag) {
}
// Allows the user to tag a tag by making a new card for the tag that contains the tagged card
// A card can have its .guild set to true, in which case the guild is the card .name
// Returns true if the tag was tagged/categorized
export async function tagTagInteractive(tag, allTaggedCards) {
console.log('Tag: ', tag)
const answer = await askQuestionText('Type a tag to categorize (or Enter/ESC to cancel):')
if(!answer || answer === 'ESC' || answer == '') {
return false
}
// Make a card for the tag if it doesn't already exist
let guildCard = (await getCardByName(tag, false))[0]
if(!guildCard) {
await createCard(tag)
guildCard = (await getCardByName(tag, false))[0]
if(!guildCard) {
console.log('Failed to create tag card, sorry.')
return false
}
}
await setCardProperty(guildCard.taskId, 'guild', true)
await tagToTopEchelon(answer, allTaggedCards)
return await tagCard(guildCard.taskId, answer)
}

2
scripts/tests.js

@ -2,7 +2,7 @@
// The tests actually happen so your database will be modified (future: allow switching databases or automatically switch) // The tests actually happen so your database will be modified (future: allow switching databases or automatically switch)
// The tests use an AO API file saved in the same directory; this file must be kept up-to-date // The tests use an AO API file saved in the same directory; this file must be kept up-to-date
// Maybe in the future a precompiled api.js created from api.ts can be hosted so that ao-cli does not have to compile any TypeScript // Maybe in the future a precompiled api.js created from api.ts can be hosted so that ao-cli does not have to compile any TypeScript
import { createSession, logout } from './api.js' import { createSession, logout } from '../ao-lib/api.js'
import { promptMenu } from './welcome.js' import { promptMenu } from './welcome.js'
async function testLoginAndOut() { async function testLoginAndOut() {

20
scripts/util.js

@ -1,20 +0,0 @@
// General helper functions
// Returns a random int between min and max (inclusive)
export function randomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Returns a random item from the given array
export function selectRandom(arrayToChooseFrom) {
return arrayToChooseFrom[randomInt(0, arrayToChooseFrom.length - 1)]
}
// Waits for the given number of milliseconds (or a brief pause by default)
export function sleep(ms = 550) {
return new Promise((r) => setTimeout(r, ms))
}
export const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]'

11
scripts/welcome.js

@ -1,7 +1,7 @@
import chalk from 'chalk' import chalk from 'chalk'
//import inquirer from 'inquirer' //import inquirer from 'inquirer'
import prompts from 'prompts' import prompts from 'prompts'
import { selectRandom } from './util.js' import { selectRandom } from '../ao-lib/util.js'
import { greenChalk, theAO, theMenu, headerStyle } from './styles.js' import { greenChalk, theAO, theMenu, headerStyle } from './styles.js'
// Different sets of messages that can be randomly selected from // Different sets of messages that can be randomly selected from
@ -22,7 +22,7 @@ const welcomeMessages = [
`A black cat crosses your path. Most people wouldn't notice, but you know you have entered the AO.`, `A black cat crosses your path. Most people wouldn't notice, but you know you have entered the AO.`,
`Dipping your brush in ink, you draw a perfect circle. This is ${theAO}.`, `Dipping your brush in ink, you draw a perfect circle. This is ${theAO}.`,
`You are offered a choice between two pills. However, you have secretly built up an immunity to both pills, and trick your opponent into taking one. Inconceviably, you are in ${theAO}.`, `You are offered a choice between two pills. However, you have secretly built up an immunity to both pills, and trick your opponent into taking one. Inconceviably, you are in ${theAO}.`,
`A young man with spiky hair and golden skin appears before you in a halo of light. He guides you to ${theAO}.`, `A young man with spiky hair and glowing skin appears before you in a halo of light. He guides you to ${theAO}.`,
`Looking for a shortcut, you worm your way through through the hedges, and, after struggling through the brush, emerge into a sunny estate garden. You've found the AO.`, `Looking for a shortcut, you worm your way through through the hedges, and, after struggling through the brush, emerge into a sunny estate garden. You've found the AO.`,
`You find a small animal burrow dug-out near the riverside. Crawling in, you find a network of caves that lead to ${theAO}.`, `You find a small animal burrow dug-out near the riverside. Crawling in, you find a network of caves that lead to ${theAO}.`,
`You receive a handwritten letter in the mail, which reads, in fine calligraphy:, "Dear —, You are in ${theAO}."`, `You receive a handwritten letter in the mail, which reads, in fine calligraphy:, "Dear —, You are in ${theAO}."`,
@ -119,7 +119,7 @@ export async function askQuestionText(prompt = 'Please enter a string:', promptO
} }
Object.assign(options, promptOptions) Object.assign(options, promptOptions)
const answer = await prompts(options) const answer = await prompts(options)
return answer.value || 'ESC' return answer.value || false
} }
export async function promptMenu(choices, prompt = 'Please choose:', hint = '(Use arrow keys)', defaultValue = null, warningMessage = null, numbered = false) { export async function promptMenu(choices, prompt = 'Please choose:', hint = '(Use arrow keys)', defaultValue = null, warningMessage = null, numbered = false) {
@ -155,6 +155,11 @@ export async function promptMenu(choices, prompt = 'Please choose:', hint = '(Us
if(typeof answer.value === 'string') { if(typeof answer.value === 'string') {
return answer.value return answer.value
} }
if(!isNaN(answer.value) && (answer.value < 0 || answer.value > choices.length) || answer.value === false) {
return false
} else if(isNaN(answer.value)) {
return answer.value
}
const chosenOption = choices[answer.value].value || choices[answer.value].title const chosenOption = choices[answer.value].value || choices[answer.value].title
if(!chosenOption) { if(!chosenOption) {
return choices[answer.value] return choices[answer.value]

2
scripts/wizard.js

@ -1,7 +1,7 @@
// Functions related to intelligently installing the AO as a whole. Specific additional feature modules are each a file under ./features. // Functions related to intelligently installing the AO as a whole. Specific additional feature modules are each a file under ./features.
import path from 'path' import path from 'path'
import { execSync } from 'child_process' import { execSync } from 'child_process'
import { aoEnv, setAoEnv } from './settings.js' import { aoEnv, setAoEnv } from '../ao-lib/settings.js'
import { detectOS, updateSoftware, isInstalled } from './system.js' import { detectOS, updateSoftware, isInstalled } from './system.js'
import { isFolder, isFile } from './files.js' import { isFolder, isFile } from './files.js'
import { aoIsInstalled } from './features/ao-server.js' import { aoIsInstalled } from './features/ao-server.js'

Loading…
Cancel
Save