Browse Source

priorities feature works! but you must have an AO server set up and running to use it.

main
deicidus 2 years ago
parent
commit
ac6c19ae3a
  1. 5
      README.md
  2. 128
      index.js
  3. 80
      scripts/api.js
  4. 212
      scripts/cards.js
  5. 4
      scripts/console.js
  6. 43
      scripts/features/alchemy.js
  7. 2
      scripts/features/ao-cli.js
  8. 21
      scripts/features/ao-server.js
  9. 1
      scripts/features/index.js
  10. 3
      scripts/priority.js
  11. 152
      scripts/system.js
  12. 3
      scripts/welcome.js

5
README.md

@ -38,15 +38,18 @@ These features are planned and many are mocked up in the menus:
* Easily update all your remote AOs at once * Easily update all your remote AOs at once
* Easily install your preferred flavor of Unix on any unsecured Windows computer given its IP address (j/k) * Easily install your preferred flavor of Unix on any unsecured Windows computer given its IP address (j/k)
* Full interactive wizard to walk you through setting up and connecting new AO hardware resources to your AO server * Full interactive wizard to walk you through setting up and connecting new AO hardware resources to your AO server
* Terminal spellbook to save and trade your favorite UNIX commands
* AO server using AO features via ao-cli command line switches (with optional sound notifications on server computer)
## Important Locations ## Important Locations
* `~/.ao/` Your AO saved data folder * `~/.ao/` Your AO saved data folder
* `~/.ao/database.sqlite3` Location of your AO database (copy to back up) * `~/.ao/database.sqlite3` Location of your AO database (copy to back up)
* `~/.ao/memes/` Files here will be loaded as card attachments when the AO server starts up
* `~/ao-cli/` Typical location for `ao-cli` * `~/ao-cli/` Typical location for `ao-cli`
* `~/ao-svelte/` Typical location for `ao-svelte` * `~/ao-svelte/` Typical location for `ao-svelte`
* `~/ao-3/` Typical location for `ao-3` * `~/ao-3/` Typical location for `ao-3`
* `~/.ao/ao-manual/` Typical location of the AO manual (Markdown files) * `~/.ao/manual/` Typical location of the AO manual (Markdown files)
* `~/Alchemy/` Typical location of Zen's Alchemy * `~/Alchemy/` Typical location of Zen's Alchemy
## Version History ## Version History

128
index.js

@ -2,7 +2,7 @@
import chalk from 'chalk' import chalk from 'chalk'
import inquirer from 'inquirer' import inquirer from 'inquirer'
import { execSync } from 'child_process' import { execSync } from 'child_process'
import { detectOS, updateSoftware, installRequired, setNodeVersion } from './scripts/system.js' import { detectOS, updateSoftware, createAoDirectories, installRequired, setNodeVersion } 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 './scripts/settings.js'
import { unicornPortal, asciiArt, clearScreen, spinnerWait } from './scripts/console.js' import { unicornPortal, asciiArt, clearScreen, spinnerWait } from './scripts/console.js'
import { welcome, exclaim, roger, farewell } from './scripts/welcome.js' import { welcome, exclaim, roger, farewell } from './scripts/welcome.js'
@ -15,10 +15,13 @@ import './scripts/strings.js'
// Import AO modular features // Import AO modular features
import * as features from './scripts/features/index.js' import * as features from './scripts/features/index.js'
import { aoIsInstalled } from './scripts/features/ao-server.js'
import { startPublicBootstrap } from './scripts/bootstrap.js' import { startPublicBootstrap } from './scripts/bootstrap.js'
import { isLoggedIn, loginPrompt, logout } from './scripts/session.js' import { isLoggedIn, loginPrompt, logout } from './scripts/session.js'
import { AO_DEFAULT_HOSTNAME } from './scripts/api.js' import { AO_DEFAULT_HOSTNAME } from './scripts/api.js'
import { getTopPriorityText } from './scripts/priority.js' import { getTopPriorityText } from './scripts/priority.js'
import { cardMenu } from './scripts/cards.js'
// These should become .env variables that are loaded intelligently // These should become .env variables that are loaded intelligently
let distro let distro
@ -27,7 +30,7 @@ let memberName
// This does not work // This does not work
function exitIfRoot() { function exitIfRoot() {
try { try {
execSync('[ "$EUID" -eq 0 ]') execSync('if [ "$EUID" -eq 0 ] then echo 1')
console.log(`${chalk.red.bold(exclaim())} Seems you're running this script as a superuser.`) console.log(`${chalk.red.bold(exclaim())} Seems you're running this script as a superuser.`)
console.log('That might cause some issues with permissions and whatnot. Run this script as your default user (without sudo) and I\'ll ask you when I need superuser permissions') console.log('That might cause some issues with permissions and whatnot. Run this script as your default user (without sudo) and I\'ll ask you when I need superuser permissions')
process.exit(1) process.exit(1)
@ -39,8 +42,8 @@ async function mainMenu() {
console.log(`\n${headerStyle('AO Main Menu')}\n`) console.log(`\n${headerStyle('AO Main Menu')}\n`)
let mainMenuChoices = [ let mainMenuChoices = [
'AO', 'AO',
'Features',
'Alchemy', 'Alchemy',
'Configure',
'Manual', 'Manual',
'Exit' 'Exit'
] ]
@ -56,7 +59,7 @@ async function mainMenu() {
case 'AO': case 'AO':
while(await useAoMenu()) {} while(await useAoMenu()) {}
break break
case 'Features': case 'Configure':
do { do {
previousChoice = await featuresMenu(previousChoice) previousChoice = await featuresMenu(previousChoice)
} while(previousChoice !== false) } while(previousChoice !== false)
@ -95,13 +98,18 @@ async function useAoMenu() {
console.log(`\n${headerStyle('AO')}\n`) 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'))
console.log('Top priority:', await getTopPriorityText()) const topPriority = await getTopPriorityText()
if(topPriority) {
console.log('Top priority:', topPriority)
} else {
console.log('Error contacting server, is your AO server running? AO features might not work.')
}
} }
let aoMenuChoices = [] let aoMenuChoices = []
if(loggedIn) { if(loggedIn) {
aoMenuChoices.push( aoMenuChoices.push(
'Chat',
'Deck', 'Deck',
'Chat',
) )
} }
aoMenuChoices.push( aoMenuChoices.push(
@ -116,12 +124,13 @@ async function useAoMenu() {
pageSize: aoMenuChoices.length pageSize: aoMenuChoices.length
}) })
switch(answer.ao_menu) { switch(answer.ao_menu) {
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'])
while(await cardMenu()) {}
break
case 'Chat': case 'Chat':
while(await chatMenu()) {} while(await chatMenu()) {}
break break
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'])
break
case 'Log In': case 'Log In':
console.log('\nao-cli will use the AO API to log into the AO server at', (aoEnv('AO_CLI_TARGET_HOSTNAME') || AO_DEFAULT_HOSTNAME) + '.') console.log('\nao-cli will use the AO API to log into the AO server at', (aoEnv('AO_CLI_TARGET_HOSTNAME') || AO_DEFAULT_HOSTNAME) + '.')
await loginPrompt() await loginPrompt()
@ -206,8 +215,9 @@ async function adminMenu() {
console.log(`\n${headerStyle('System Alchemy')}`) console.log(`\n${headerStyle('System Alchemy')}`)
const adminChoices = [ const adminChoices = [
'Update system software', 'Update system software',
'AO install wizard',
'Switch AO target server', 'Switch AO target server',
'Import/Export state/decks', 'Import/export state/decks',
'Watch logs now', 'Watch logs now',
'Tests', 'Tests',
'Update remote AOs', 'Update remote AOs',
@ -224,6 +234,12 @@ async function adminMenu() {
case 'Update system software': case 'Update system software':
updateSoftware() updateSoftware()
break break
case 'AO install wizard':
await aoInstallWizard()
break
case 'Install other AO version':
await chooseAoVersion()
break
case 'Switch AO target server': case 'Switch AO target server':
case 'Import/Export state/decks': case 'Import/Export state/decks':
case 'Watch logs now': case 'Watch logs now':
@ -239,6 +255,89 @@ async function adminMenu() {
return true return true
} }
// Friendly interactive install wizard walks you through the entire process of installing and configuring a version of the AO and its features
async function aoInstallWizard() {
asciiArt('AO Installer')
console.log('Welcome to the AO installer. The Coalition of Invisible Colleges is currently in licensing negotiations between the Autonomous Organization and Zen to acquire rights to display Zen\'s welcome text here, which is better than this irony.')
const level = await chooseInstallLevel()
if(!level) {
console.log('Install canceled.')
return
}
console.log('Proceeding with', level, 'installation.')
const version = await chooseAoVersion()
if(!version) {
console.log('Install canceled.')
return
}
// Ask them how they would like to host the AO (private on this computer only, public via tor only, public website via HTTPS/SSL)
updateSoftware()
createAoDirectories()
installRequired()
setNodeVersion()
if(!aoIsInstalled(version)) {
installAo(version)
}
//configureAO() // set required ENV variables (are any still required? make all optional?)
if(level === 'standard' || level === 'full') {
//if(!features.bitcoin.isInstalled()) features.bitcoin.install()
//if(!features.lightning.isInstalled()) features.lightning.install()
console.log('Skipping manual, tor, bitcoin, lightning, jitsi, and configuration of themes, glossary, jubilee (coming soon)')
}
if(level === 'full') {
console.log('Skipping youtube-dl, Signal, borg (coming soon)') //maybe can just loop through all feature modules here
}
console.log('Skipping SSL/Certbot (coming soon)') // Ask them at the beginning but do it here
console.log('The AO is installed.')
}
// Asks if the user wants to do a minimal, standard, or full install and returns their answer
async function chooseInstallLevel() {
const answer = await inquirer.prompt({
name: 'level_menu',
type: 'list',
message: 'What kind of installation?',
choices: [
{ name: 'Minimal'.padEnd(11) + 'only core AO web server', value: 'minimal', short: 'minimal install' },
{ name: 'Standard'.padEnd(11) + 'most AO features installed (recommended)', value: 'standard', short: 'standard install' },
{ name: 'Full'.padEnd(11) + 'all AO features installed', value: 'full', short: 'full install' },
{ name: 'Cancel', value: false }
]
})
return answer.level_menu
}
// Detects which version(s) of the AO are installed (ao-3, ao-react, or ao-v)
// todo: Maybe should move this to an option under the ao-server feature menu
function detectAoVersion() {
return aoEnv(AO_VERSION)
}
// Asks whether the user wants to install ao-svelte, ao-3, or ao-cli (only) and returns their choice
async function chooseAoVersion() {
console.log(`\n${headerStyle('Choose AO Version')}`)
console.log('Active version:', aoEnv('AO_VERSION'))
const answer = await inquirer.prompt({
name: 'version_menu',
type: 'list',
message: 'Please choose:',
choices: [
{ name: 'ao-svelte'.padEnd(12) + 'new and mobile-first, currently in prototype phase', value: 'ao-svelte', short: 'ao-svelte' },
{ name: 'ao-3'.padEnd(12) + 'the original, created in Vue 3, polished and bug-free', value: 'ao-3', short: 'ao-3' },
{ name: 'ao-cli only'.padEnd(12), value: 'ao-cli' },
'Cancel'
]
})
if(answer.version_menu === 'Cancel') {
return false
}
setAoEnv('AO_VERSION', answer.version_menu)
// todo: If the version has changed, install it now
console.log('AO version choice saved.')
return answer.version_menu
}
// Prints the AO Unit Tests Menu and executes the user's choice // Prints the AO Unit Tests Menu and executes the user's choice
async function testsMenu() { async function testsMenu() {
console.log(`\n${headerStyle('AO Unit Tests')}`) console.log(`\n${headerStyle('AO Unit Tests')}`)
@ -304,7 +403,9 @@ async function featuresMenu(previousMenuChoice = 0) {
if(answer.features_menu === 'Back to Main Menu') { if(answer.features_menu === 'Back to Main Menu') {
return false return false
} }
while(await oneFeatureMenu(answer.features_menu, features[answer.features_menu])) {} const chosenFeature = features[answer.features_menu]
const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer.features_menu
while(await oneFeatureMenu(chosenFeatureName, chosenFeature)) {}
return answer.features_menu return answer.features_menu
} }
@ -417,11 +518,6 @@ async function todoList(title, todoItems) {
}) })
} }
// Detects which version(s) of the AO are installed (ao-3, ao-react, or ao-v)
function detectAoVersion() {
}
// Returns false if a flag means the program should now terminate // Returns false if a flag means the program should now terminate
// -v Print version info // -v Print version info
async function handleArgs(args) { async function handleArgs(args) {

80
scripts/api.js

@ -18,14 +18,14 @@ export const socket = io(AO_SOCKET_URL, {
}) })
// Load the current session cookies from the AO .env file // Load the current session cookies from the AO .env file
let currentMemberId = aoEnv('AO_CLI_SESSION_USERNAME') let currentMemberId = aoEnv('AO_CLI_SESSION_MEMBERID')
let currentSessionId = aoEnv('AO_CLI_SESSION_ID') let currentSessionId = aoEnv('AO_CLI_SESSION_ID')
let currentSessionToken = aoEnv('AO_CLI_SESSION_TOKEN') let currentSessionToken = aoEnv('AO_CLI_SESSION_TOKEN')
// Performs a post request to the specified endpoint, sending the given payload // Performs a post request to the specified endpoint, sending the given payload
export async function postRequest(endpoint, payload) { export async function postRequest(endpoint, payload, verbose = true) {
if (!currentSessionToken) { if (!currentSessionToken) {
console.log('Session token not set, API not ready.') if(verbose) console.log('Session token not set, API not ready.')
return new Promise(() => null) return new Promise(() => null)
} }
try { try {
@ -41,14 +41,15 @@ export async function postRequest(endpoint, payload) {
.set('session', currentSessionId) .set('session', currentSessionId)
} }
} catch (err) { } catch (err) {
console.log('request failed', err) if(verbose) console.log('request failed', err)
return null return null
} }
} }
// Performs a post request to the /event endpoint, sending the given JSON object as the event // Performs a post request to the /event endpoint, sending the given JSON object as the event
export async function postEvent(event) { export async function postEvent(event, verbose) {
return await postRequest('/event', event) console.log('about to post event')
return await postRequest('/events', event, verbose)
} }
// Attempts login with the given username and password combo. If successful, returns the generated session and token (login cookies). // Attempts login with the given username and password combo. If successful, returns the generated session and token (login cookies).
@ -186,12 +187,30 @@ export async function cacheMeme(taskId) {
} }
// Cards feature // Cards feature
// Returns the card and other cards as specified by the alsoGetRelevant arg
// If multiple cards are returned, they will be returned in their global deck order (global creation order on server)
export async function getCard(taskId, alsoGetRelevant = 'subcards') { export async function getCard(taskId, alsoGetRelevant = 'subcards') {
taskId = taskId.trim().toLowerCase() taskId = taskId.trim().toLowerCase()
let payload = { taskId: taskId } let payload = { taskId: taskId }
const result = await postRequest('/fetchTaskByID', payload) // todo: change to flat text, not JSON const result = await postRequest('/fetchTaskByID', payload, false) // todo: change to flat text, not JSON (?)
if(!result || !result.body) { if(!result || !result.body) {
console.log('Error fetching task.') //console.log('Error fetching task.')
return null
}
if(alsoGetRelevant) {
let relevantCards = await getAllRelevantCards(result.body, alsoGetRelevant)
return [result.body, ...relevantCards]
}
return [result.body]
}
// Cards feature
export async function getCardByName(taskName, alsoGetRelevant = 'subcards') {
taskName = taskName.trim()
let payload = { taskName: taskName }
const result = await postRequest('/fetchTaskByName_exact', payload, false) // todo: change to flat text, not JSON (?)
if(!result || !result.body || result.statusCode === 204 || result.statusCode === 400) {
//console.log('Error fetching task.')
return null return null
} }
if(alsoGetRelevant) { if(alsoGetRelevant) {
@ -263,6 +282,22 @@ export async function getAllRelevantCards(
} }
} }
export async function createCard(
name,
anonymous = false,
prioritized = false
) {
return await postEvent({
type: 'task-created',
name: name,
color: 'blue',
deck: (anonymous || !currentMemberId) ? [] : [currentMemberId],
inId: anonymous ? null : currentMemberId || null,
prioritized: prioritized,
}, false)
}
// This is different from only setting a card property because when a card's color changes, it is bumped to the top of the .subTasks color pile
export async function colorCard(taskId, color) { export async function colorCard(taskId, color) {
return await postEvent({ return await postEvent({
type: 'task-colored', type: 'task-colored',
@ -273,6 +308,7 @@ export async function colorCard(taskId, color) {
}) })
} }
// Set arbitrary metadata on a card
export async function setCardProperty(taskId, property, value) { export async function setCardProperty(taskId, property, value) {
return await postEvent({ return await postEvent({
type: 'task-property-set', type: 'task-property-set',
@ -293,6 +329,7 @@ export async function passCard(taskId, toMemberId) {
}) })
} }
// Send an immediate bark and notification to the member if possible, reminding them that they have an unread message from you (no automated pings)
export async function remindMember(memberId) { export async function remindMember(memberId) {
return await postEvent({ return await postEvent({
type: 'member-reminded', type: 'member-reminded',
@ -394,7 +431,7 @@ export async function dropPile(taskId) {
export async function prioritizeCard(taskId, inId, position = 0) { export async function prioritizeCard(taskId, inId, position = 0) {
return await postEvent({ return await postEvent({
type: 'task-prioritized', type: 'task-prioritized',
taskId, taskId: taskId,
inId: inId, inId: inId,
position: position, position: position,
blame: currentMemberId blame: currentMemberId
@ -900,31 +937,6 @@ console.log("NODE_ENV is", process.env.NODE_ENV)*/
return Promise.resolve(false) return Promise.resolve(false)
}*/ }*/
/*
export async createCard(
name,
anonymous
) {
const act = {
type: 'task-created',
name: name,
color: 'blue',
deck: anonymous
? []
: aoStore.member && currentMemberId
? [currentMemberId]
: [],
inId: anonymous ? null : aoStore.memberCard.taskId || null,
prioritized: false,
}
// console.log('AO: client/api.ts: createCard: ', {
// act,
// 'aoStore.memberCard': aoStore.memberCard,
// })
return await postEvent(act)
}
*/
/* /*
export async createCardIfDoesNotExist( export async createCardIfDoesNotExist(
name, name,

212
scripts/cards.js

@ -0,0 +1,212 @@
// Cards module - everything related to cards should go here (database install is automatic for AO server so no feature module)
import inquirer from 'inquirer'
import { aoEnv } from './settings.js'
import { getCard, getCardByName, createCard, prioritizeCard, completeCard, uncheckCard, refocusCard } from './api.js'
import { headerStyle } from './styles.js'
// The card menu is complex and so has been split into this separate file
export async function cardMenu() {
console.log(`\n${headerStyle('My Deck')}`)
const cardChoices = [
{ name: 'Top priorities', value: 'priorities', short: 'priorities' }, // hand? (7) (add #s in parens)
{ name: 'Cards in hand', value: 'subcards', short: 'hand' }, // (current) deck? (60)
{ name: 'Browse full deck', value: 'browse', short: 'browse' }, // archive? (10,000)
'Back to AO Menu'
]
const answer = await inquirer.prompt({
name: 'card_menu',
type: 'list',
message: 'Please choose:',
choices: cardChoices,
pageSize: cardChoices.length,
})
switch(answer.card_menu) {
case 'priorities':
while(await prioritiesMenu()) {}
break
case 'subcards':
while(await subcardsMenu()) {}
break
case 'browse':
while(await browseMenu()) {}
break
default:
return false
}
return true
}
// Displays the priorities of the given taskId in a menu. Selecting a card shows a menu for that card. If taskId is null, member card is used.
async function prioritiesMenu(taskId = null) {
console.log(`\n${headerStyle('My Priorities')}`)
let prioritiesChoices = []
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
if(!memberId) {
console.log('Not logged in.')
return false
}
if(!taskId) {
// Get the priorities of my member card
taskId = memberId
}
const fetchedCards = await getCard(taskId, 'priorities')
if(!fetchedCards || fetchedCards.length < 1) {
console.log('Failed to fetch member card, this is bad.')
return false
}
const card = fetchedCards[0]
const priorityCards = fetchedCards.slice(1) // First card is member card itself
let priorities = card.priorities.slice()
priorities.reverse()
console.log('You have', priorityCards.length, 'priorities:')
prioritiesChoices = priorities.map((priorityTaskId, i) => {
const priorityCard = priorityCards.find(p => p.taskId === priorityTaskId)
if(!priorityCard) {
return 'Missing card, repair your database'
}
return {
name: priorityCard.name,
value: { index: i, card: priorityCard },
short: priorityCard.name.substring(0, 70) + priorityCard.name.length >= 70 ? '...' : ''
}
})
prioritiesChoices.push(
{ name: 'Create priority', value: 'create_here', short: 'new priority' },
{ name: 'Back to AO Menu', value: false, short: 'back' }
)
const answer = await inquirer.prompt({
name: 'priorities_menu',
type: 'rawlist',
message: 'Please choose:',
choices: prioritiesChoices,
loop: false
})
switch(answer.priorities_menu) {
case false:
return false
case 'create_here':
let previousCardCreatedText
do {
console.log('previousCardCreatedText is', previousCardCreatedText)
previousCardCreatedText = await createCardInteractive()
} while(previousCardCreatedText != '\n')
return true
case 'Missing card, repair your database':
console.log('Database repair yet implemented, sorry.')
return true
}
let chosenTask = answer.priorities_menu.card
const chosenTaskId = chosenTask.taskId
let previousAnswer
do {
previousAnswer = await priorityCardMenu(chosenTask, answer.priorities_menu.index)
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)
console.log('Card menu not yet implemented.')
return true
}
// 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
async function priorityCardMenu(card, index) {
if(!card) {
console.log('priorityCardMenu: 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)
console.log(`\n${headerStyle('Priority: ' + card.name)}`)
let priorityChoices = []
if(index != 0) {
priorityChoices.push({ name: 'Upboat', value: 'upboat', short: 'upboat' })
}
priorityChoices.push(
{ name: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' },
{ name: 'Downboat', value: 'downboat', short: 'downboat' },
//{ name: 'Browse within', value: 'browse', short: 'browse' }
{ name: 'Back to Priorities', value: false, short: 'back' }
)
const answer = await inquirer.prompt({
name: 'priority_card_menu',
type: 'list',
message: 'Please choose:',
choices: priorityChoices,
pageSize: priorityChoices.length,
})
switch(answer.priority_card_menu) {
case 'check':
if(isChecked) {
await uncheckCard(taskId)
} else {
await completeCard(taskId)
}
break
case 'upboat':
await prioritizeCard(taskId, memberId)
return false
case 'downboat':
await refocusCard(taskId, memberId)
return false
case 'browse':
break
default:
return false
}
return true
}
async function subcardsMenu() {
console.log('Not yet implemented')
}
async function browseMenu() {
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
async function createCardInteractive(prioritized = true) {
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID')
if(!memberId) {
console.log('Not logged in.')
return false
}
const answer = await inquirer.prompt({
name: 'new_card_text',
type: 'input',
message: 'New card or Enter to end:',
})
if(answer.new_card_text.trim().length <= 0) {
return false
}
// Check if the card alerady exists
const fetchedCards = await getCardByName(answer.new_card_text, false)
if(fetchedCards && fetchedCards.length >= 1) {
if(fetchedCards.length >= 2) {
console.log('More than one copy of this card was found. This should not happen.')
}
if(prioritized) {
console.log('Card already exists, prioritizing.')
const prioritizeResult = prioritizeCard(fetchedCards[0].taskId, memberId)
if(!prioritizeResult.ok) {
console.log('May have failed to prioritize card.')
}
}
return false
}
console.log('card does not exist yet. creating...', answer.new_card_text)
const result = await createCard(answer.new_card_text, false, true)
return answer.new_card_text
}

4
scripts/console.js

@ -19,8 +19,8 @@ export async function unicornPortal(ms) {
// Prints the given message to the screen in the given ASCII art style. Here is a list of decent styles: // Prints the given message to the screen in the given ASCII art style. Here is a list of decent styles:
const asciiFonts = ['Standard', 'Digital', 'Bubble', 'Script', 'Mini', 'Banner', 'Alphabet', 'Avatar', 'Chunky', 'Computer', 'Contessa', 'Gothic', 'Invita', 'Lockergnome', 'Madrid', 'Morse', 'Moscow', 'Pawp', 'Pepper', 'Pyramid', 'Rectangles', 'Shadow', 'Short', 'Slant', 'Small', 'Stampatello', 'Stop', 'Straight', 'Thick', 'Thin', 'Weird'] const asciiFonts = ['Standard', 'Digital', 'Bubble', 'Script', 'Mini', 'Banner', 'Alphabet', 'Avatar', 'Chunky', 'Computer', 'Contessa', 'Gothic', 'Invita', 'Lockergnome', 'Madrid', 'Morse', 'Moscow', 'Pawp', 'Pepper', 'Pyramid', 'Rectangles', 'Shadow', 'Short', 'Slant', 'Small', 'Stampatello', 'Stop', 'Straight', 'Thick', 'Thin', 'Weird']
export async function asciiArt(message, style) { export async function asciiArt(message, style) {
const randomFont = selectRandom(asciiFonts) if(!style) style = selectRandom(asciiFonts)
let art = figlet.textSync(message || 'Autonomous Organization', { font: randomFont }) let art = figlet.textSync(message || 'Autonomous Organization', { font: style })
art = centerLines(art) art = centerLines(art)
console.log(gradient.pastel.multiline(art)) console.log(gradient.pastel.multiline(art))
} }

43
scripts/features/alchemy.js

@ -0,0 +1,43 @@
import { execSync } from 'child_process'
import { lsFolder } from '../files.js'
export const ALCHEMY_FOLDER = process.env.HOME + '/Alchemy'
function statusAlchemy() {
return lsFolder(ALCHEMY_FOLDER).length >= 6 ? 'installed' : 'off'
}
function downloadAlchemy() {
console.log('Beacon of Zen')
}
function updateAlchemy() {
try {
const result = execSync('cd ~/Alchemy && git pull')
if(result.toString().includes('Already up to date.')) {
console.log('Alchemy is already up to date.')
} else {
console.log('Alchemy updated.')
return true
}
} catch(error) {
console.log('Failed to update Alchemy scripts: ', error)
}
return false
}
function onMyCustomMenuItem() {
console.log("Not implemented.")
}
export default {
name: 'Alchemy',
description: 'scripts that transmute your system into gold',
status: statusAlchemy,
install: downloadAlchemy,
update: updateAlchemy,
// These menu items will show up oin Features->Alchemy. The key/(menu:value:) is arbitrary but must be the same in both places.
custom_script_1: onMyCustomMenuItem,
menu: [
{ name: 'Menu item to trigger a very specific Alchemy script', value: 'custom_script_1' }
]
}

2
scripts/features/ao-cli.js

@ -29,7 +29,7 @@ function installAoCli() {
} }
async function getAoCliVersion() { async function getAoCliVersion() {
const packageJson = await loadJsonFile(path.join(__dirname, '../../package.json')) const packageJson = execSync('npx ao-cli -v')
return packageJson.version return packageJson.version
} }

21
scripts/features/ao-server.js

@ -14,7 +14,26 @@ function serviceStatus() {
return 'off' return 'off'
} }
// Return true if the specified AO repo exists: ao-svelte or ao-3 are expected values
export function aoIsInstalled(version) {
console.log('aoIsInstalled not implemented yet')
return true
}
function installAo(version) {
if(!version) {
version = aoEnv('AO_VERSION')
if(!version) {
version = 'ao-svelte'
setAoEnv('AO_VERSION', 'ao-svelte')
console.log('No AO server/frontend version specified, defaulting to ao-svelte.')
}
}
console.log('todo: git clone the correct repo now')
}
export default { export default {
description: 'AO server instance on this computer', description: 'AO server instance on this computer',
status: serviceStatus status: serviceStatus,
install: installAo,
} }

1
scripts/features/index.js

@ -1,4 +1,5 @@
// Import the features modules in this folder, which each add, remove, and admininster one AO feature // Import the features modules in this folder, which each add, remove, and admininster one AO feature
export { default as alchemy } from './alchemy.js'
export { default as 'ao-cli' } from './ao-cli.js' export { default as 'ao-cli' } from './ao-cli.js'
export { default as 'ao-server' } from './ao-server.js' export { default as 'ao-server' } from './ao-server.js'
export { default as bitcoin } from './bitcoin.js' export { default as bitcoin } from './bitcoin.js'

3
scripts/priority.js

@ -9,6 +9,9 @@ export async function getTopPriorityText() {
return 'Not logged in' return 'Not logged in'
} }
const fetchedCards = await getCard(memberId, 'priority') const fetchedCards = await getCard(memberId, 'priority')
if(fetchedCards === null) {
return null
}
if(!fetchedCards || fetchedCards.length < 2) { if(!fetchedCards || fetchedCards.length < 2) {
return 'None' return 'None'
} }

152
scripts/system.js

@ -53,22 +53,139 @@ export function updateSoftware() {
console.log(`(You may need to input your ${chalk.blue.bold("'sudo' password")} here)`) console.log(`(You may need to input your ${chalk.blue.bold("'sudo' password")} here)`)
switch(distro) { switch(distro) {
case 'debian': case 'debian':
execSync('sudo apt update && sudo apt autoremove && sudo apt upgrade') execSync('sudo apt update -yqqq && sudo apt autoremove -yqqq && sudo apt upgrade -yqqq')
break break
case 'arch': case 'arch':
execSync('sudo pacman -Syu --noconfirm') execSync('sudo pacman -Syu --noconfirm')
// for Manjaro, also do pamac upgrade -a && pamac update --aur --devel to do normal then build all AUR packages (check https://forum.manjaro.org/c/announcements/11 first)
break break
case 'fedora': case 'fedora':
execSync('sudo dnf update && sudo dnf upgrade') execSync('sudo dnf update -yqqq 2>/dev/null && && sudo dnf autoremove -yqqq && sudo dnf upgrade -yqqq')
break break
case 'mac': case 'mac':
execSync('install && sudo brew update') // Install homebrew (todo: if not installed)
execSync('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"')
execSync('install && sudo brew update') // is this right? can it replicate all commands such as update upgrade autoremove?
break break
} }
return true return true
} }
// Checks if the given package is installed using the standard repo for your detected OS
function isInstalled(packageName, group) {
detectOS()
if(!distro) {
console.log("Your OS was not recognized, so nothing was updated, sorry.")
return false
}
switch(distro) {
case 'debian':
return execSync("dpkg-query -W -f='${Status}' " + packageName + " 2>/dev/null").includes('ok installed')
case 'arch':
try {
if(!group) {
const stdout = execSync("pacman -Qi " + packageName)
if(stdout.includes('was not found')) {
return false
}
} else {
const stdout = execSync("pacman -Qg " + packageName).toString()
if(!stdout.includes(packageName)) {
return false
}
}
return true
} catch(error) {
break
}
case 'fedora':
case 'mac':
}
}
// Uses the standard app repo for your detected OS to install the named package
function installPackage(packageName) {
detectOS()
if(!distro) {
console.log("Your OS was not recognized, so nothing was updated, sorry.")
return false
}
switch(distro) {
case 'debian':
try {
execSync('sudo apt install -y ' + packageName)
return true
} catch(error) {
break
}
case 'arch':
try {
const result = execSync("sudo pacman -S " + packageName + " --noconfirm").toString()
console.log(packageName, 'installed result is', result)
return true
} catch(error) {
break
}
case 'fedora':
case 'mac':
console.log('Install on this OS not yet implemented, sorry.')
}
console.log('Failed to install', packageName + '. Error:', error)
return false
}
// Installs the specified package or packages using the standard repos for your detected OS. Prints console messages if verbose.
function installIfNotInstalled(packageNameOrNames, verbose = true, group = false) {
if(!Array.isArray(packageNameOrNames)) {
if(typeof packageNameOrNames !== 'string') {
if(verbose) console.log('Invalid package name provided. Doing nothing.')
return null
}
packageNameOrNames = [ packageNameOrNames ]
}
let packagesInstalled = 0
let packagesFailed = 0
packageNameOrNames.forEach(packageName => {
if(!isInstalled(packageName, group)) {
const success = installPackage(packageName)
if(success) {
if(verbose) console.log('Installed', packageName + '.')
packagesInstalled++
} else {
if(verbose) console.log('Failed to install', packageName + '.')
packagesFailed++
}
} else {
if(verbose) console.log(packageName, 'already installed.')
}
})
return { installed: packagesInstalled, failed: packagesFailed }
}
// Creates the directories to store the AO's database, memes, manual, and maybe other things, ~/.ao by standard
export function createAoDirectories() {
try {
execSync('mkdir -p $HOME/.ao/memes')
} catch(error) {
console.log('Error creating ~/.ao/memes directory. Maybe it already exists.')
}
}
function installNvm() {
try {
execSync('[ -z $NVM_DIR ]')
execSync('source ~/Alchemy/ingredients/iron && install_nvm')
console.log(`Installed nvm.`)
return true
} catch(err) {
return false
}
}
// Installs core dependencies required by Alchemy and the AO // Installs core dependencies required by Alchemy and the AO
export function installRequired() { export function installRequired() {
detectOS() detectOS()
@ -79,37 +196,28 @@ export function installRequired() {
console.log('Installing Alchemy and AO installation process core dependencies (fast if already installed)...') console.log('Installing Alchemy and AO installation process core dependencies (fast if already installed)...')
console.log(`(You may need to input your ${chalk.blue.bold("'sudo' password")} here)`) console.log(`(You may need to input your ${chalk.blue.bold("'sudo' password")} here)`)
// Install on every OS
installIfNotInstalled(['curl', 'wget', 'git', 'make', 'sqlite3', 'python', 'autoconf-archive'])
installNvm()
// Install OS-specific requirements // Install OS-specific requirements
switch(distro) { switch(distro) {
case 'debian': case 'debian':
execSync('sudo apt install build-essential')
// Some of these might not be required // Some of these might not be required
execSync('source ~/Alchemy/ingredients/lead && install_if_needed sqlite3 zlib1g-dev libtool-bin autoconf autoconf-archive automake autotools-dev libgmp-dev libsqlite3-dev python python3 python3-mako libsodium-dev build-essential pkg-config libev-dev libcurl4-gnutls-dev libssl-dev fakeroot devscripts') installIfNotInstalled(['build-essential', 'zlib1g-dev', 'libtool-bin', 'autoconf', 'automake autotools-dev', 'libgmp-dev', 'libsqlite3-dev', 'python3', 'python3-mako', 'libsodium-dev', 'pkg-config', 'libev-dev', 'libcurl4-gnutls-dev', 'libssl-dev', 'fakeroot', 'devscripts'])
break break
case 'arch': case 'arch':
try { installIfNotInstalled('base-devel', true, true)
execSync('[[ ! $(pacman -Qg base-devel) ]]') installIfNotInstalled(['gmp', 'pkgconf', 'libev', 'python-mako', 'python-pip', 'net-tools', 'zlib', 'libsodium', 'gettext', 'nginx'])
execSync('sudo pacman -S base-devel --noconfirm')
} catch(err) {}
execSync('source ~/Alchemy/ingredients/lead && install_if_needed python gmp sqlite3 autoconf-archive pkgconf libev python-mako python-pip net-tools zlib libsodium gettext nginx')
break break
case 'fedora': case 'fedora':
execSync('source ~/Alchemy/ingredients/lead && install_if_needed sqlite3 autoconf autoconf-archive automake python python3 python3-mako pkg-config fakeroot devscripts') installIfNotInstalled(['autoconf', 'automake', 'python3', 'python3-mako', 'pkg-config', 'fakeroot', 'devscripts'])
break break
} }
// Install on every OS
execSync('source ~/Alchemy/ingredients/lead && install_if_needed git wget make')
try {
execSync('[ -z $NVM_DIR ]')
execSync('source ingredients/iron && install_nvm')
console.log(`Installed nvm.`)
} catch(err) {}
return true return true
} }
// Sets node to the current version used by the AO // Sets node to the current version used by the AO
export function setNodeVersion() { export function setNodeVersion() {
execSync('source ingredients/iron && set_node_to v16.13.0') execSync('source ~/Alchemy/ingredients/lead && source ~/Alchemy/ingredients/iron && set_node_to v16.13.0')
} }

3
scripts/welcome.js

@ -25,7 +25,8 @@ const welcomeMessages = [
`You enter a gap between two hedges and, after struggling through the brush, emerge into a sunny estate garden. You've found the AO.`, `You enter a gap between two 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}."`,
`An unexpected calm settles over you. The ${greenChalk.bold('AO')} is here: Here is ${theAO}.` `An unexpected calm settles over you. The ${greenChalk.bold('AO')} is here: Here is ${theAO}.`,
`You plant a seed, and from that one seed grow many trees. The forest and the tradition of tree-planting are ${theAO}.`
] ]
const menuMessages = [ const menuMessages = [
`You see ${theMenu}:`, `You see ${theMenu}:`,

Loading…
Cancel
Save