diff --git a/README.md b/README.md index 7ac3c30..4cefed9 100644 --- a/README.md +++ b/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 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 +* 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 * `~/.ao/` Your AO saved data folder * `~/.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-svelte/` Typical location for `ao-svelte` * `~/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 ## Version History diff --git a/index.js b/index.js index 5e86827..670d780 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ import chalk from 'chalk' import inquirer from 'inquirer' 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 { unicornPortal, asciiArt, clearScreen, spinnerWait } from './scripts/console.js' import { welcome, exclaim, roger, farewell } from './scripts/welcome.js' @@ -15,10 +15,13 @@ import './scripts/strings.js' // Import AO modular features import * as features from './scripts/features/index.js' +import { aoIsInstalled } from './scripts/features/ao-server.js' + import { startPublicBootstrap } from './scripts/bootstrap.js' import { isLoggedIn, loginPrompt, logout } from './scripts/session.js' import { AO_DEFAULT_HOSTNAME } from './scripts/api.js' import { getTopPriorityText } from './scripts/priority.js' +import { cardMenu } from './scripts/cards.js' // These should become .env variables that are loaded intelligently let distro @@ -27,7 +30,7 @@ let memberName // This does not work function exitIfRoot() { 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('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) @@ -39,8 +42,8 @@ async function mainMenu() { console.log(`\n${headerStyle('AO Main Menu')}\n`) let mainMenuChoices = [ 'AO', - 'Features', 'Alchemy', + 'Configure', 'Manual', 'Exit' ] @@ -56,7 +59,7 @@ async function mainMenu() { case 'AO': while(await useAoMenu()) {} break - case 'Features': + case 'Configure': do { previousChoice = await featuresMenu(previousChoice) } while(previousChoice !== false) @@ -95,13 +98,18 @@ async function useAoMenu() { console.log(`\n${headerStyle('AO')}\n`) if(loggedIn) { 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 = [] if(loggedIn) { aoMenuChoices.push( - 'Chat', 'Deck', + 'Chat', ) } aoMenuChoices.push( @@ -116,12 +124,13 @@ async function useAoMenu() { pageSize: aoMenuChoices.length }) 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': while(await chatMenu()) {} 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': 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() @@ -206,8 +215,9 @@ async function adminMenu() { console.log(`\n${headerStyle('System Alchemy')}`) const adminChoices = [ 'Update system software', + 'AO install wizard', 'Switch AO target server', - 'Import/Export state/decks', + 'Import/export state/decks', 'Watch logs now', 'Tests', 'Update remote AOs', @@ -224,6 +234,12 @@ async function adminMenu() { case 'Update system software': updateSoftware() break + case 'AO install wizard': + await aoInstallWizard() + break + case 'Install other AO version': + await chooseAoVersion() + break case 'Switch AO target server': case 'Import/Export state/decks': case 'Watch logs now': @@ -239,6 +255,89 @@ async function adminMenu() { 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 async function testsMenu() { console.log(`\n${headerStyle('AO Unit Tests')}`) @@ -304,7 +403,9 @@ async function featuresMenu(previousMenuChoice = 0) { if(answer.features_menu === 'Back to Main Menu') { 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 } @@ -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 // -v Print version info async function handleArgs(args) { diff --git a/scripts/api.js b/scripts/api.js index 31b783a..62b9cce 100644 --- a/scripts/api.js +++ b/scripts/api.js @@ -18,14 +18,14 @@ export const socket = io(AO_SOCKET_URL, { }) // 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 currentSessionToken = aoEnv('AO_CLI_SESSION_TOKEN') // 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) { - console.log('Session token not set, API not ready.') + if(verbose) console.log('Session token not set, API not ready.') return new Promise(() => null) } try { @@ -41,14 +41,15 @@ export async function postRequest(endpoint, payload) { .set('session', currentSessionId) } } catch (err) { - console.log('request failed', err) + if(verbose) console.log('request failed', err) return null } } // Performs a post request to the /event endpoint, sending the given JSON object as the event -export async function postEvent(event) { - return await postRequest('/event', event) +export async function postEvent(event, verbose) { + 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). @@ -186,12 +187,30 @@ export async function cacheMeme(taskId) { } // 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') { taskId = taskId.trim().toLowerCase() 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) { - 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 } 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) { return await postEvent({ 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) { return await postEvent({ 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) { return await postEvent({ type: 'member-reminded', @@ -394,7 +431,7 @@ export async function dropPile(taskId) { export async function prioritizeCard(taskId, inId, position = 0) { return await postEvent({ type: 'task-prioritized', - taskId, + taskId: taskId, inId: inId, position: position, blame: currentMemberId @@ -900,31 +937,6 @@ console.log("NODE_ENV is", process.env.NODE_ENV)*/ 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( name, diff --git a/scripts/cards.js b/scripts/cards.js new file mode 100644 index 0000000..ee7f67a --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/scripts/console.js b/scripts/console.js index 671cf4d..21bfe05 100644 --- a/scripts/console.js +++ b/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: 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) { - const randomFont = selectRandom(asciiFonts) - let art = figlet.textSync(message || 'Autonomous Organization', { font: randomFont }) + if(!style) style = selectRandom(asciiFonts) + let art = figlet.textSync(message || 'Autonomous Organization', { font: style }) art = centerLines(art) console.log(gradient.pastel.multiline(art)) } diff --git a/scripts/features/alchemy.js b/scripts/features/alchemy.js new file mode 100644 index 0000000..ce4e259 --- /dev/null +++ b/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' } + ] +} diff --git a/scripts/features/ao-cli.js b/scripts/features/ao-cli.js index 1625638..8a12622 100644 --- a/scripts/features/ao-cli.js +++ b/scripts/features/ao-cli.js @@ -29,7 +29,7 @@ function installAoCli() { } async function getAoCliVersion() { - const packageJson = await loadJsonFile(path.join(__dirname, '../../package.json')) + const packageJson = execSync('npx ao-cli -v') return packageJson.version } diff --git a/scripts/features/ao-server.js b/scripts/features/ao-server.js index 9136529..c8ad1ae 100644 --- a/scripts/features/ao-server.js +++ b/scripts/features/ao-server.js @@ -14,7 +14,26 @@ function serviceStatus() { 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 { description: 'AO server instance on this computer', - status: serviceStatus -} \ No newline at end of file + status: serviceStatus, + install: installAo, +} diff --git a/scripts/features/index.js b/scripts/features/index.js index 580c23f..b591227 100644 --- a/scripts/features/index.js +++ b/scripts/features/index.js @@ -1,4 +1,5 @@ // 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-server' } from './ao-server.js' export { default as bitcoin } from './bitcoin.js' diff --git a/scripts/priority.js b/scripts/priority.js index ddad035..d006eeb 100644 --- a/scripts/priority.js +++ b/scripts/priority.js @@ -9,6 +9,9 @@ export async function getTopPriorityText() { return 'Not logged in' } const fetchedCards = await getCard(memberId, 'priority') + if(fetchedCards === null) { + return null + } if(!fetchedCards || fetchedCards.length < 2) { return 'None' } diff --git a/scripts/system.js b/scripts/system.js index 5d80cff..d7fc5ec 100644 --- a/scripts/system.js +++ b/scripts/system.js @@ -53,20 +53,137 @@ export function updateSoftware() { console.log(`(You may need to input your ${chalk.blue.bold("'sudo' password")} here)`) switch(distro) { 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 case 'arch': 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 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 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 + } + + 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 } +} - return true +// 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 @@ -79,37 +196,28 @@ export function installRequired() { 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)`) + // Install on every OS + installIfNotInstalled(['curl', 'wget', 'git', 'make', 'sqlite3', 'python', 'autoconf-archive']) + installNvm() + // Install OS-specific requirements switch(distro) { case 'debian': - execSync('sudo apt install build-essential') // 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 case 'arch': - try { - execSync('[[ ! $(pacman -Qg base-devel) ]]') - 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') + installIfNotInstalled('base-devel', true, true) + installIfNotInstalled(['gmp', 'pkgconf', 'libev', 'python-mako', 'python-pip', 'net-tools', 'zlib', 'libsodium', 'gettext', 'nginx']) break 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 } - - // 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 } // Sets node to the current version used by the AO 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') } diff --git a/scripts/welcome.js b/scripts/welcome.js index 142a1f9..5f5e1d9 100644 --- a/scripts/welcome.js +++ b/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 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}."`, - `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 = [ `You see ${theMenu}:`, @@ -107,4 +108,4 @@ export async function askQuestionText(prompt = 'Please enter a string:', promptO Object.assign(options, promptOptions) const answer = await inquirer.prompt(options) return answer.text -} \ No newline at end of file +}