diff --git a/ao-lib/api.js b/ao-lib/api.js index d5c9711..0d5ac5d 100644 --- a/ao-lib/api.js +++ b/ao-lib/api.js @@ -67,6 +67,7 @@ export async function postRequest(endpoint, payload = null, verbose = true) { // Performs a post request to the /event endpoint, sending the given JSON object as the event export async function postEvent(event, verbose) { + event = { ...event, blame: currentMemberId } return await postRequest('/events', event, verbose) } @@ -164,7 +165,7 @@ export async function bootstrap(serverOnion = null) { return onionList } -export async function shadowchat(room, message, username) { +/*export async function shadowchat(room, message, username) { return await postEvent({ type: 'shadowchat', room: room, @@ -227,6 +228,7 @@ export async function mute() { export async function unmute() { return await updateMemberField('muted', false) } +*/ // Memes feature export async function fetchMeme(memeHash, progressCallback) { @@ -271,29 +273,30 @@ export async function uploadMemes(formData, progressCallback) { }) } -export async function cacheMeme(taskId) { +/*export async function cacheMeme(taskId) { return await postEvent({ type: 'meme-cached', 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, false) // todo: change to flat text, not JSON (?) + let payload = { taskIds: [ taskId ] } + const result = await postRequest('/fetchTasks', payload, false) if(!result || !result.body) { //console.log('Error fetching task.') return null } + let fetchedCard = result.body[0] if(alsoGetRelevant) { - let relevantCards = await getAllRelevantCards(result.body, alsoGetRelevant) - return [result.body, ...relevantCards] + let relevantCards = await getAllRelevantCards(fetchedCard, alsoGetRelevant) + return [fetchedCard, ...relevantCards] } - return [result.body] + return [fetchedCard] } // Cards feature @@ -327,12 +330,14 @@ export async function getAllRelevantCards( if(existingTasks === undefined) { existingTasks = new Map() } - let taskIdsToFetch + let taskIdsToFetch = [] // Choose which taskIds we are going to request from the server switch (scope) { case 'priority': - taskIdsToFetch = new Set([seedTask.priorities.at(-1)]) + if(seedTask.hasOwnProperty('priorities') && seedTask.priorities?.length >= 1) { + taskIdsToFetch = new Set([seedTask.priorities.at(-1)]) + } break case 'priorities': taskIdsToFetch = new Set(seedTask.priorities) @@ -374,7 +379,7 @@ export async function getAllRelevantCards( } } -export async function createCard( +/*export async function createCard( name, anonymous = false, prioritized = false @@ -573,14 +578,14 @@ export async function tagCard(taskId, newTitle) { guild: newTitle, blame: currentMemberId }) -} +}*/ export async function getCardsForTag(tag) { return (await postRequest('/fetchTasksByGuild', { guild: tag })).body.cards } // Checkmarks feature -export async function completeCard(taskId) { +/*export async function completeCard(taskId) { return await postEvent({ type: 'task-claimed', taskId: taskId, @@ -657,7 +662,7 @@ export async function bookResource(taskId, startTime, endTime) { startTs: startTime, endTs: endTime }) -} +}*/ // Member account features export async function updateMemberField(field, newValue) { @@ -683,7 +688,7 @@ export async function createMember(name, fob = '') { }) } -export async function activateMember(memberId) { +/*export async function activateMember(memberId) { return await postEvent({ type: 'member-activated', memberId: memberId @@ -822,7 +827,7 @@ export async function visitCard(taskId, inChat = false, notify = false) { area: inChat ? 1 : 0, notify: notify }) -} +}*/ /* export async function markSeen(taskId) { @@ -838,7 +843,7 @@ export async function markSeen(taskId) { */ // Pinboard feature -export async function resizeGrid(taskId, newHeight, newWidth, newSize) { +/*export async function resizeGrid(taskId, newHeight, newWidth, newSize) { return await postEvent({ type: 'grid-resized', taskId: taskId, @@ -874,7 +879,7 @@ export async function removeGridFromCard(taskId) { type: 'grid-removed', taskId: taskId }) -} +}*/ // This function encodes whatever is passed by the search box as a URIComponent and passes it to a search endpoint, returning the response when supplied export async function search(querystring, take = 10, skip = 0) { @@ -883,7 +888,7 @@ export async function search(querystring, take = 10, skip = 0) { return await postRequest('/search/' + qs + params) } -export async function requestBtcQr(taskId) { +/*export async function requestBtcQr(taskId) { return await postEvent({ type: 'address-updated', taskId @@ -913,7 +918,7 @@ export async function setQuorum(quorum) { type: 'quorum-set', quorum: quorum }) -} +}*/ /*reaction( () => { diff --git a/index.js b/index.js index 02e4746..46fca1e 100644 --- a/index.js +++ b/index.js @@ -16,9 +16,9 @@ import { randomInt } from './ao-lib/util.js' import './scripts/strings.js' // Import AO modular features -import features, { featuresMenu } from './scripts/features/index.js' -import manual, { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './scripts/features/manual.js' -import aoCli from './scripts/features/ao-cli.js' +import features, { featuresMenu } from './features/index.js' +import manual, { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './features/manual.js' +import aoCli from './features/ao-cli.js' const sleep = (ms = 550) => { return new Promise((r) => setTimeout(r, ms)) } diff --git a/package-lock.json b/package-lock.json index 9c35ffb..4ee8318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.8", "license": "AGPL-3.0-or-later", "dependencies": { - "blessed": "^0.1.81", "chalk": "^5.0.1", "chalk-animation": "^2.0.2", "crypto": "^1.0.1", @@ -201,17 +200,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/blessed": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", - "integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==", - "bin": { - "blessed": "bin/tput.js" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -1772,11 +1760,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "blessed": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", - "integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==" - }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", diff --git a/package.json b/package.json index 1dc0460..f9f1d9e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "author": "Coalition of Invisible Colleges", "license": "AGPL-3.0-or-later", "dependencies": { - "blessed": "^0.1.81", "chalk": "^5.0.1", "chalk-animation": "^2.0.2", "crypto": "^1.0.1", diff --git a/scripts/ao.js b/scripts/ao.js index 2e22432..655b9c1 100644 --- a/scripts/ao.js +++ b/scripts/ao.js @@ -9,20 +9,22 @@ import { AO_DEFAULT_HOSTNAME } from '../ao-lib/api.js' import { headerStyle } from './styles.js' import { cardMenu } from './cards.js' import { connectMenu } from './connect.js' -import chatMenu from './shadowchat.js' +import chatMenu from './chat.js' import { promptMenu } from './welcome.js' +import { checkAoServerInteractive } from './wizard.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). export default async function useAoMenu() { + const aoServerIsRunning = await checkAoServerInteractive() + if(!aoServerIsRunning) { + console.log('Please start an AO server to use AO client features.') + return false + } const loggedIn = isLoggedIn() if(loggedIn) { console.log('Logged in as:', aoEnv('AO_CLI_SESSION_USERNAME')) 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.') - } + console.log('Top priority:', topPriority || 'None') } let aoMenuChoices = [] if(loggedIn) { diff --git a/scripts/bootstrap.js b/scripts/bootstrap.js index 04b1c8e..1513ef1 100644 --- a/scripts/bootstrap.js +++ b/scripts/bootstrap.js @@ -1,7 +1,7 @@ -// The bootstrapping module uses the glossary in peers.json (later will use members from DB?) +// The bootstrapping module uses the glossary in ~/.ao/peers.json (later will use members from DB?) // to look up tor addresses for the give shortname or SSH public key. // We could just do all this in the AO, but the bootstrapper is for public / loose ties and the AO's explicit p2p is for close / private ties. -// The other main difference is that the AO stores data, and the chat server does not (ao-cli only uses database for Use AO Features). +// The other main difference is that the AO stores data in the database, simplex-chat does not (ao-cli only uses database for Use AO Features). // The bootstrapper occasionally queries all of the tor addresses in your address book. // If they are an AO with bootstrapping turned on, the AO server will respond with its public directory information. // Since you have connected to them via their .onion address, it is assumed they are a known trusted party, @@ -14,7 +14,7 @@ // Start bootstrapping in the background export function startPublicBootstrap() { - // Go through all the address book entries in my peers.json + // Go through all the address book entries in my ~/.ao/peers.json // For each one that has a .onion address, do a fetch on it at the /bootstrap route // If it responds with JSON containing directory information, increment the hops: field on all of it, and merge it with my file // Must use entire new or old record. Use whichever one has fewer hops. Only replace if timestamp is newer. diff --git a/scripts/cards.js b/scripts/cards.js index 67af6a7..64b28e1 100644 --- a/scripts/cards.js +++ b/scripts/cards.js @@ -1,5 +1,5 @@ // 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 '../ao-lib/api.js' +import { getCardByName, postEvent } from '../ao-lib/api.js' import { headerStyle } from './styles.js' import { prioritiesMenu } from './priority.js' import { subcardsMenu } from './hand.js' @@ -72,6 +72,13 @@ export async function createCardInteractive(prioritized = false) { return answer } console.log('card does not exist yet. creating...', answer) - const result = await createCard(answer, false, prioritized) + const result = await postEvent({ + type: 'task-created', + name: answer, + color: 'blue', + deck: [memberId], + inId: memberId, + prioritized: prioritized, + }) return answer } diff --git a/scripts/connect.js b/scripts/connect.js index 6b8bcba..742a95d 100644 --- a/scripts/connect.js +++ b/scripts/connect.js @@ -2,8 +2,8 @@ import { headerStyle } from './styles.js' import { aoEnv, setAoEnv } from '../ao-lib/settings.js' import { isLoggedIn } from './session.js' -import { isInstalled } from './features/tor.js' -import { connectToAo, getAoBootstrapList, bootstrap } from '../ao-lib/api.js' +import { isInstalled } from '../features/tor.js' +import { postEvent, getAoBootstrapList, bootstrap } from '../ao-lib/api.js' import { roger, promptMenu } from './welcome.js' // Prints a menu to connect your AO to other AOs and manage connections @@ -98,7 +98,11 @@ async function connectInteractive() { const [onion, secret] = answer.connection_string.split(':') console.log('onion is', onion, 'and secret is', secret) console.log('Attempting connect...') - const result = await connectToAo(onion, secret) + const result = await postEvent({ + type: 'ao-outbound-connected', + address: onion, + secret: secret + }) console.log('result is', result.body) return true } diff --git a/scripts/features/alchemy.js b/scripts/features/alchemy.js deleted file mode 100644 index ce4e259..0000000 --- a/scripts/features/alchemy.js +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 4cab634..0000000 --- a/scripts/features/ao-cli.js +++ /dev/null @@ -1,140 +0,0 @@ -import { execSync } from 'child_process' -import { fileURLToPath } from 'url' -import path from 'path' -import fs from 'fs' -import { loadJsonFile } from '../files.js' - -// Can't include .json files without adding an experimental node flag, but we can use this workaround to use require, which works, instead -import { createRequire } from "module"; // Bring in the ability to create the 'require' method -const require = createRequire(import.meta.url); // construct the require method -const packageVersion = require("../../package.json").version - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -// Returns one of: off, installed, enabled, running, synced, error -function cliStatus() { - try { - const stdout = execSync('npm list -g @autonomousorganization/ao-cli') - const isAoCliInstalled = stdout.includes('@autonomousorganization/ao-cli@') - if(isAoCliInstalled) return 'installed' - } catch(err) { - return 'error' - } - return 'off' -} - -// It is possible to run ao-cli with npx @autonomousorganization/ao-cli. In this case, it can help you install it permanently. -function installAoCli() { - try { - execSync('npm i -g @autonomousorganization/ao-cli 2>&1') - console.log('Installed ao-cli.') - } catch(err) { - console.log('Error installing ao-cli:', err) - } -} - -async function getAoCliVersion() { - const npmGlobalPackagesPath = execSync('npm root -g').toString().replace(/\n/, '') - const aoGlobalPackagePath = path.join(npmGlobalPackagesPath, '@autonomousorganization/ao-cli/package.json') - let jsonFileContents - try { - const contents = fs.readFileSync(aoGlobalPackagePath) - jsonFileContents = JSON.parse(contents) - } catch(err) { - if(err.code === 'ENOENT') { - console.log('The global ao-cli package.json file does not exist.') - } else { - console.log('Unknown error loading global ao-cli package.json file, aborting.', err) - } - return null - } - if(!jsonFileContents.hasOwnProperty('version')) { - return null - } - return jsonFileContents.version -} - -// Updates the globally-installed version of this package, ao-cli, using npm -async function selfUpdate() { - try { - const beforeVersionNumber = await getAoCliVersion() - const result = execSync('npm update -g @autonomousorganization/ao-cli 2>&1') - const afterVersionNumber = await getAoCliVersion() - if(beforeVersionNumber === afterVersionNumber) { - console.log("ao-cli version is already current.") - } else { - console.log('\nao-cli self-updated automatically from version', beforeVersionNumber, 'to version', afterVersionNumber, 'from the official npm repository.') - } - } catch (err) { - console.log('Failed to update ao-cli: ', err) - } -} - -// Returns true if the 'ao' alias for ao-cli has already been addded to .bashrc -function checkAoAlias() { - try { - execSync('grep "ao=ao-cli" ~/.bashrc') - } catch(err) { - return 'off' - } - return 'installed' -} - -// Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line -function installAoAlias() { - try { - execSync('echo alias ao=ao-cli >> $HOME/.bashrc') - console.log('Added alias line to ~/.bashrc. You can now type \'ao\' to launch ao-cli.') - } catch(err) { - console.log('Failed to add alias, sorry.') - } -} - - -// Returns true if the cd hook function has already been addded to .bashrc -function checkCdHook() { - try { - execSync('grep "function cd \{ builtin cd.*--interstitial.* ; \}$" ~/\.bashrc') - } catch(err) { - return 'off' - } - return 'installed' -} - -// Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line -function installCdHook() { - try { - execSync('echo \'function cd { builtin cd "\$@" && node \~/ao-cli \-\-interstitial "\$\@" ; }\' >> $HOME/.bashrc') - console.log('Added alias line to ~/.bashrc. Try typing \'cd\' in a terminal.') - } catch(err) { - console.log('Failed to add alias, sorry.') - } -} - -function removeCdHook() { - try { - let result = execSync('sed -i \'/^function cd.*/d\' ' + process.env.HOME + '/.bashrc') - console.log('sed:', result.toString()) - } catch(error) { - console.log(error.output.toString()) - } -} - -export default { - description: 'this AO command-line interface', - status: cliStatus, - install: installAoCli, - version: getAoCliVersion, - update: selfUpdate, - add_alias: installAoAlias, - remove_alias: () => console.log("Not implemented yet."), - add_cd: installCdHook, - remove_cd: removeCdHook, - menu: [ - { name: () => checkAoAlias() === 'installed' ? 'Remove \'ao\' shortcut for \'ao-cli\'' : 'Install \'ao\' shortcut for \'ao-cli\'', - value: () => checkAoAlias() === 'installed' ? 'remove_alias' : 'add_alias' }, - { name: () => checkCdHook() === 'installed' ? 'Remove \'cd\' fantasy hook' : 'Install \'cd\' fantasy hook', - value: () => checkCdHook() === 'installed' ? 'remove_cd' : 'add_cd' }, - ] -} diff --git a/scripts/features/ao-server.js b/scripts/features/ao-server.js deleted file mode 100644 index a581e6e..0000000 --- a/scripts/features/ao-server.js +++ /dev/null @@ -1,96 +0,0 @@ -import { execSync, exec } from 'child_process' -import { isFolder } from '../files.js' -import path from 'path' -import { unlink } from 'node:fs' -import SystemServiceManager from '../services.js' - -const AO_SERVER_PATH = path.join(process.env.HOME, 'ao-server') - -const aoServerServiceTemplate = -`[Unit] -Description=ao-server daemon - -[Service] -WorkingDirectory=${process.env.HOME}/ao-server -ExecStart=npm run build && node --loader ts-node/esm ${process.env.HOME}/ao-server/src/server/app.ts -User=${process.env.USER} -Type=simple -Restart=on-failure -PrivateTmp=true - -[Install] -WantedBy=multi-user.target` - -const aoServerServiceManager = new SystemServiceManager('ao-server', aoServerServiceTemplate) - -// Returns one of: off, installed, enabled, running, synced, error -function serviceStatus() { - if(!aoIsInstalled()) { - return 'off' - } - if(aoServerServiceManager.isInstalled()) { - const isRunning = aoServerServiceManager.isRunning() - switch(isRunning) { - case true: - return 'running' - case 'error': - return 'error' - } - return 'enabled' - } else { - return 'installed' - } -} - -// Downloads ao-server to ~/ao-server. Returns false if it fails, which usually means the folder already exists (update instead). -export function downloadAoServer() { - try { - execSync('git clone http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-server.git 2>&1') - } catch(err) { - switch(err.code) { - case 128: - return false - } - } - return true -} - -// Return true if the ~/ao-server/.git folder exists -export function aoIsInstalled() { - return isFolder(path.join(AO_SERVER_PATH, '.git')) -} - -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.') - } - } - downloadAoServer() -} - -export function updateAoServer() { - console.log('Updating ao-server. If you are a developer, you may be asked for your SSH key now.') - try { - const stdout = execSync('cd ' + AO_SERVER_PATH + ' && git pull origin main 2>&1') - if(stdout.includes('Already up to date.')) { - console.log('Already up to date.') - return - } - console.log('\nao-server was updated.') - } catch(error) { - console.log('git pull failed with error:', error) - } -} - -export default { - description: 'AO server instance on this computer', - status: serviceStatus, - install: installAo, - isInstalled: aoIsInstalled, - update: updateAoServer, - submodules: [ aoServerServiceManager ] -} diff --git a/scripts/features/bitcoin.js b/scripts/features/bitcoin.js deleted file mode 100644 index 5e30f2b..0000000 --- a/scripts/features/bitcoin.js +++ /dev/null @@ -1,20 +0,0 @@ -import { execSync } from 'child_process' - -// Returns one of: off, installed, enabled, running, synced, error -export function bitcoinStatus() { - try { - const stdout = execSync('source ~/Alchemy/ingredients/lead && source ~/Alchemy/ingredients/gold && bitcoin_is_synced') - const isSynced = stdout.includes('Bitcoin is synced!') - if(isSynced) return 'synced' - else if(stdout.includes('error')) return 'error' - } catch(err) { - return 'error' - } - return 'off' -} - -export default { - name: 'Bitcoin', - description: 'payments', - status: bitcoinStatus -} diff --git a/scripts/features/borg.js b/scripts/features/borg.js deleted file mode 100644 index addd5f4..0000000 --- a/scripts/features/borg.js +++ /dev/null @@ -1,7 +0,0 @@ -// BorgBackup module - -export default { - name: 'Borg', - description: 'encrypted-in-transit, deduplicated incremental backup (over tor)', - status: () => null, -} diff --git a/scripts/features/certbot.js b/scripts/features/certbot.js deleted file mode 100644 index f86a360..0000000 --- a/scripts/features/certbot.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - name: 'SSL/Certbot', - description: 'HTTPS for public web AO', - status: () => null, -} diff --git a/scripts/features/encryption.js b/scripts/features/encryption.js deleted file mode 100644 index a60c7d0..0000000 --- a/scripts/features/encryption.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - name: 'Encryption', - description: 'serverside secret messages', //encrypt messages to and from this computer', - status: () => null, -} diff --git a/scripts/features/files.js b/scripts/features/files.js deleted file mode 100644 index 5cc2a92..0000000 --- a/scripts/features/files.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - name: 'File hosting', - description: 'file attachments on cards (sync p2p via tor with other AOs)', - status: () => null, -} diff --git a/scripts/features/glossary.js b/scripts/features/glossary.js deleted file mode 100644 index 3339795..0000000 --- a/scripts/features/glossary.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - name: 'Glossary', - description: 'custom glossary', - status: () => null, -} diff --git a/scripts/features/index.js b/scripts/features/index.js deleted file mode 100644 index a4e0368..0000000 --- a/scripts/features/index.js +++ /dev/null @@ -1,197 +0,0 @@ -// Re-export the features modules in this folder, which each add, remove, and admininster one AO feature (imported in project index.js) -// Also contains the Features menus to control these features in this directory -import chalk from 'chalk' -import fs from 'fs' -import { lsFolder } from '../files.js' -import { fileURLToPath } from 'url' -import path from 'path' -import { headerStyle, greenChalk, styledStatus } from '../styles.js' -import { spinner } from '../console.js' -import SystemServiceManager, { getCustomServicesList, addCustomServiceInteractive, removeCustomService } from '../services.js' -import { promptMenu } from '../welcome.js' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -const loadFeatures = async () => { - - let filenames = lsFolder(path.join(__dirname)) - let features = {} - for(let i = 0; i < filenames.length; i++) { - const filename = filenames[i] - if(filename === 'index.js') continue - const moduleShortname = filename.replace(/\.js$/, '') - const path = './' + filename - features[moduleShortname] = (await import(path)).default - } - return features -} - -const features = await loadFeatures() -export default features - -// Prints the Configure AO Features menu and executes the user's choice -let featuresChoices -export async function featuresMenu(previousMenuChoice = 0) { - const stopSpinner = spinner('Loading status...') - let loadedFeatures = 0 - if(!featuresChoices) { - featuresChoices = Object.entries(features).map(([featureKey, feature]) => { - let featureName = featureKey - if(feature.hasOwnProperty('name') && feature.name.length >= 1) { - featureName = feature.name - } - const nameColumn = featureName.padEnd(17) - const status = feature.status() || 'Unknown' - if(status !== 'Unknown') { - loadedFeatures++ - } - const statusColumn = styledStatus(status, 25) - const descriptionColumn = feature.description || '' - const choice = { title: nameColumn + statusColumn + descriptionColumn, value: featureKey, short: featureKey } - return choice - }) - featuresChoices.push({ title: '---', disabled: true }) - const customServices = getCustomServicesList() - customServices.forEach(serviceName => { - const nameColumn = serviceName.padEnd(17) - const service = new SystemServiceManager(serviceName) - const status = service.status() || 'Unknown' - const statusColumn = styledStatus(status, 25) - const descriptionColumn = service.description || '' - const choice = { title: nameColumn + statusColumn + descriptionColumn, value: 'service_' + serviceName, short: serviceName } - featuresChoices.push(choice) - }) - featuresChoices.push( - { title: 'Add custom service', value: 'add_service' }, - 'Back to Main Menu' - ) - } else { - loadedFeatures = featuresChoices.filter(feature => { - return typeof feature === 'object' && feature.hasOwnProperty('name') && !feature.name.includes('Unknown') - }).length - } - stopSpinner('Loaded status for ' + loadedFeatures + '/' + (featuresChoices.length - 1) + ' features.') - const answer = await promptMenu(featuresChoices, 'Configure AO Features', undefined, previousMenuChoice) - if(answer.includes('service_')) { - const serviceName = answer.features_menu.replace(/^service_/, '') - const service = new SystemServiceManager(serviceName) - const nameWithoutDot = serviceName.split('.')[0] + ' service' - console.log('calling service menu with true') - await oneFeatureMenu(nameWithoutDot, service, true) - return answer.features_menu - } - switch(answer) { - case 'add_service': - console.log('Many Linux distributions run system services in the background. You can add an existing systemctl service to the AO Features menu to make it easier to start and stop your services. You must know the name of the service and it must already exist.') - while(await addCustomServiceInteractive()) {} - featuresChoices = null - return true - case 'Back to Main Menu': - return false - } - const chosenFeature = features[answer] - const chosenFeatureName = (chosenFeature.hasOwnProperty('name') && chosenFeature.name.length >= 1) ? chosenFeature.name : answer - while(await oneFeatureMenu(chosenFeatureName, chosenFeature)) {} - return answer -} - -// Prints the menu options for a specific feature (including subfeatures) -function oneFeatureMenuChoices(name, feature, status, isCustom = false) { - if(!status && status !== false) { - status = typeof feature.status === 'function' ? feature.status() : feature.hasOwnProperty('status') ? feature.status : 'off' - } - let featureChoices = [] - if(!status) { - console.log("This AO subfeature module lacks a status() function, not sure which menu items to display.") - return null - } - - const installed = typeof feature.isInstall === 'function' ? feature.isInstalled : feature.hasOwnProperty('isInstalled') ? feature.isInstalled : status !== 'off' - const running = installed && typeof feature.isRunning === 'function' ? feature.isRunning() : feature.hasOwnProperty('isRunning') ? feature.isRunning : false - if(running && typeof feature.stop === 'function') { - featureChoices.push({ title: 'Stop ' + name, value: feature.stop }) - } else if(installed && !running && typeof feature.start === 'function') { - featureChoices.push({ title: 'Start ' + name, value: feature.start }) - } - - if(status === 'off') { - if(typeof feature.install === 'function') { - featureChoices.push({ title: 'Install ' + name, value: feature.install }) - } - } else { - if(typeof feature.update === 'function') { - featureChoices.push({ title: 'Update ' + name, value: feature.update }) - } - if(typeof feature.uninstall === 'function') { - featureChoices.push({ title: 'Uninstall ' + name, value: feature.uninstall }) - } - } - - if(isCustom) { - featureChoices.push({ title: 'Remove from list', value: () => { - const nameWithoutLabel = name.replace(/ service/, '') - removeCustomService(nameWithoutLabel) - featuresChoices = null - } - }) - } - - if(feature.hasOwnProperty('menu')) { - feature.menu.forEach(menuItem => { - const menuItemName = typeof menuItem.name === 'function' ? menuItem.name() : menuItem.name - if(menuItemName) { - featureChoices.push({ title: menuItemName, value: menuItem.Value }) - } - // todo: uninstall option will go here also - }) - } - return featureChoices -} - -// Prints the feature menu for a specific feature -// Each feature module can export functions for status, install, version, update, displayed in the menu based on context -// If the module also has a menu: field, these menu items will each be appended if the name is truthy when calculated -// Prints all the standard-named features plus features listed under 'menu' -// Each feature can also have subfeatures/featuare submodules. These are simply features not listed in the main AO Features list -// Instead, their (usually shorter list of) menu items are listed flattened alongside the feature's menu items (no submenus) -// This has better usability than submenus, giving users more contexual cues at the same time about where they are in the menus -export async function oneFeatureMenu(name, feature, isCustom = false) { - console.log(`\n${headerStyle(name)}`) - if(feature.description && feature.description?.length >= 1) { - console.log('\n' + feature.description + '\n') - } - let featureChoices = [] - const stopSpinner = spinner('Loading status...') - const status = feature?.status() || false - featureChoices = oneFeatureMenuChoices(name, feature, status, isCustom) - if(feature.submodules && Array.isArray(feature.submodules)) { - feature.submodules.forEach(subfeature => { - const submoduleChoices = oneFeatureMenuChoices(subfeature.name, subfeature, undefined, isCustom) - if(submoduleChoices && submoduleChoices.length >= 1) { - featureChoices = featureChoices.concat(submoduleChoices) - } - }) - } - stopSpinner('Loaded ' + name + ' status: ' + (feature.status() || 'Unknown') + '\n') - if(featureChoices.length < 1) { - console.log("Nothing to do yet on this feature, please check back soon.") - return false - } - featureChoices.push( - 'Back to Features' - ) - const answer = await promptMenu(featureChoices, name) - if(answer === 'Back to Features') { - return false - } - if(typeof answer === 'function') { - await answer() - return true - } else if(Object.keys(feature).includes(answer)) { - await feature[answer]() - return true - } - console.log('Not yet implemented') - return true -} diff --git a/scripts/features/jitsi.js b/scripts/features/jitsi.js deleted file mode 100644 index 5c3b49b..0000000 --- a/scripts/features/jitsi.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - name: 'Jitsi', - description: 'secure video chat', - status: () => null, -} diff --git a/scripts/features/jubilee.js b/scripts/features/jubilee.js deleted file mode 100644 index 369c61c..0000000 --- a/scripts/features/jubilee.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - name: 'Jubilee', - description: 'monthly points creation event', - status: () => null, -} diff --git a/scripts/features/lightning.js b/scripts/features/lightning.js deleted file mode 100644 index de7f423..0000000 --- a/scripts/features/lightning.js +++ /dev/null @@ -1,23 +0,0 @@ -import { execSync } from 'child_process' - -// Returns one of: off, installed, enabled, running, synced, error -export function lightningStatus() { - try { - const stdout = execSync('lightning-cli -V') - } catch(err) { - return 'off' - } - - try { - const stdout = execSync('lightning-cli getinfo') - } catch(err) { - return 'installed' - } - - return 'running' -} - -export default { - description: 'payments', - status: lightningStatus -} \ No newline at end of file diff --git a/scripts/features/manual.js b/scripts/features/manual.js deleted file mode 100644 index aa08bc2..0000000 --- a/scripts/features/manual.js +++ /dev/null @@ -1,184 +0,0 @@ -// Functions for downloading, updating, and displaying the AO Manual, a hierarchy of markdown files -import chalk from 'chalk' -import { execSync, exec } from 'child_process' -import { loadYamlMarkdownFile, lsFolder, isFolder } from '../files.js' -import { repeatString, centerLines } from '../strings.js' -import { promptMenu } from '../welcome.js' -import { headerStyle, manualTitleStyle } from '../styles.js' -import { basename } from 'path' -import { marked } from 'marked' -import TerminalRenderer from 'marked-terminal' - -export const AO_MANUAL_PATH = process.env.HOME + '/.ao/manual' - -export function manualStatus() { - // There are at least eighteen items in the manual - if(lsFolder(AO_MANUAL_PATH).length >= 18) { - return 'installed' - } - return 'off' -} - -// Downloads the ao-manual repo to ~/.ao/manual/. Returns false if it fails, which usually means the folder already exists (update instead). -export function downloadManual() { - try { - execSync('git clone http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-manual.git ' + AO_MANUAL_PATH + ' 2>&1') - } catch(err) { - switch(err.code) { - case 128: - return false - } - } - return true -} - -export async function updateManual() { - exec('cd ' + process.env.HOME + '/.ao/manual && git pull origin main 2>&1', (error, stdout, stderr) => { - //console.log('error:', error, 'stdout:', stdout, 'stderr:', stderr) - if(error) { - console.log('git pull failed with error:', error) - } - if(stdout.includes('Already up to date.')) { - return - } - console.log('/nAO User Manual was updated.') - }) -} - -// Removes numbered prefix such as 12_ and .md suffix, replaces underscores with spaces, and adds titlecase -function formatManualTitleString(title) { - // Remove .md suffix - if(/\.md$/.exec(title)) { - title = title.substring(0, title.length - 3) - } - // Remove numbered prefix e.g., 12_ - if(/^\d*_/.exec(title)) { - title = title.split('_').slice(1).join('_') - } - // Replace underscores with spaces - title = title.replaceAll('_', ' ') - - return title.toTitleCase() -} - - marked.setOptions({ - renderer: new TerminalRenderer({ - showSectionPrefix: false, - }) -}) - -// Given a path and file/folder name, it returns the appropriate manual title -// If it's a folder and there is an index.js inside that has a title: field, that overrides the title -// Otherwise it's the filename or foldername, minus anything before the first underscore (_), in titlecase -async function loadManualTitle(path, fileOrFolder) { - // If it's a .md file, check inside for a title: field - if(/\.md$/.exec(fileOrFolder)) { - const indexTitle = (await loadYamlMarkdownFile(path + fileOrFolder))?.meta?.title - if(indexTitle) { - return indexTitle - } - } - - // If it's a folder, check for a title: field in index.md and return if exists - if(isFolder(path + fileOrFolder)) { - const indexPath = path + fileOrFolder + '/index.md' - const indexTitle = (await loadYamlMarkdownFile(indexPath))?.meta?.title - if(indexTitle) { - return indexTitle - } - } - - // Fall back to using the file/folder name as the title - return formatManualTitleString(fileOrFolder) -} - -// Prints the specified manual page to the screen -export async function printManualPage(path, injectedTitle = '') { - if(isFolder(path)) { - path += '/index.md' - } - const dict = await loadYamlMarkdownFile(path) - const title = injectedTitle || dict?.meta?.title || formatManualTitleString(basename(path)) - const formattedTitle = manualTitleStyle(title).centerInLine(title.length).centerInConsole() - console.log('\n' + formattedTitle + '\n') - const renderedMarkdown = marked(dict?.tail).wordWrap().centerInConsole() - console.log(renderedMarkdown) -} - -// Render the manual folder or a subfolder as a menu -// First the index.js is listed using the folder name or title loaded from inside the file as the menu item title -// Next, any other files not starting with a number are loaded and displayed in discovered/arbitrary order -// Next, items starting with 0_, then 1_, and so on are displayed in order. You can mix files and folders. -// Selecting a menu item renders it. For .md files it renders it and shows the same level Manual menu again. -// For folders, it goes into that folder and renders it as a manual menu folder -// This allows arbitrarily nested Manual menu folders to be explored in a standardized menu system -export async function manualFolderAsMenu(path, menuTitle, backOption, previousMenuChoice = 0) { - if(!isFolder(path)) { - return false - } - if(path[path.length - 1] != '/') { - path += '/' - } - - let menuItems = [] - - const folderContents = lsFolder(path) - if(folderContents.some(fileOrFolder => fileOrFolder === 'index.md')) { - const indexTitle = await loadManualTitle(path, 'index.md') - - let indexMenuItem = {} - indexMenuItem[indexTitle] = 'index.md' - menuItems.push(indexMenuItem) - } - - let unNumberedItems = [] - let numberedItems = [] - const sortedFolderContents = folderContents.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) - for(let i = 0; i < sortedFolderContents.length; i++) { - const fileOrFolder = sortedFolderContents[i] - if(fileOrFolder === 'index.md') { - continue - } - const potentialNumber = fileOrFolder.split('_')[0] - const initialNumber = parseInt(potentialNumber) - const title = await loadManualTitle(path, fileOrFolder) - const menuItem = {} - menuItem[title] = fileOrFolder - - if(isNaN(initialNumber)) { - unNumberedItems.push(menuItem) - } else { - numberedItems.push(menuItem) - } - } - menuItems = menuItems.concat(unNumberedItems, numberedItems) - - const menuChoices = menuItems.map(menuItem => Object.keys(menuItem)[0]) - menuChoices.push(backOption) - if(previousMenuChoice >= menuChoices.length) { - previousMenuChoice = 0 - } - const answer = await promptMenu(menuChoices, menuTitle, 'choose a topic', previousMenuChoice, undefined, true) - const chosenMenuIndex = menuChoices.indexOf(answer) - if(answer === backOption) { - return false - } - const chosenPath = path + Object.values(menuItems.find(menuItem => Object.keys(menuItem)[0] === answer))[0] - await printManualPage(chosenPath, answer) - const newBackOption = backOption === 'Back to Main Menu' ? 'Back to Table of Contents' : 'Back to ' + menuTitle - let previousChoice = 0 - do { - previousChoice = await manualFolderAsMenu(chosenPath, answer, newBackOption, previousChoice + 1) - } - while(previousChoice !== false) - return chosenMenuIndex -} - -export default { - name: 'Manual', - description: 'AO user manual', - status: manualStatus, - install: downloadManual, - isInstalled: () => manualStatus() === 'installed', - update: updateManual -} diff --git a/scripts/features/nginx.js b/scripts/features/nginx.js deleted file mode 100644 index 5e7232b..0000000 --- a/scripts/features/nginx.js +++ /dev/null @@ -1,28 +0,0 @@ -import { execSync } from 'child_process' - -// Returns one of: off, installed, enabled, running, synced, error -export function nginxStatus() { - try { - const stdout = execSync('nginx -v 2>&1') - } catch(err) { - return 'off' - } - - try { - const stdout = execSync('systemctl status nginx') - if(stdout.includes('Active: active (running)')) { - return 'running' - } else if(stdout.includes('Active: inactive (dead)')) { - return 'installed' - } - } catch(err) { - return 'installed' - } - - return 'installed' -} - -export default { - description: 'host AO publicly over the world wide web', - status: nginxStatus -} \ No newline at end of file diff --git a/scripts/features/signal.js b/scripts/features/signal.js deleted file mode 100644 index 357539e..0000000 --- a/scripts/features/signal.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - name: 'Signal', - description: 'secure notifications', - status: () => null, -} diff --git a/scripts/features/themes.js b/scripts/features/themes.js deleted file mode 100644 index 2e4edef..0000000 --- a/scripts/features/themes.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - name: 'Themes', - description: 'custom themes', - status: () => null, -} diff --git a/scripts/features/tor.js b/scripts/features/tor.js deleted file mode 100644 index 84be360..0000000 --- a/scripts/features/tor.js +++ /dev/null @@ -1,25 +0,0 @@ -import { execSync } from 'child_process' - -// Returns one of: off, installed, enabled, running, synced, error -export function torStatus() { - try { - const stdout = execSync('systemctl status tor') - const isTorRunning = stdout.includes('Active: active (running)') - if(isTorRunning) return 'running' - else if(stdout.includes('error')) return 'error' - else if(stdout.includes('stopped')) return 'installed' - } catch(err) { - return 'error' - } - return 'off' -} - -export function isInstalled() { - return torStatus() === 'running' -} - -export default { - description: 'connect AOs p2p', - status: torStatus, - isInstalled: isInstalled -} diff --git a/scripts/features/youtube-dl.js b/scripts/features/youtube-dl.js deleted file mode 100644 index de232fb..0000000 --- a/scripts/features/youtube-dl.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - description: 'cache web videos', - status: () => null, -} diff --git a/scripts/hand.js b/scripts/hand.js index 84b66c9..c8fb322 100644 --- a/scripts/hand.js +++ b/scripts/hand.js @@ -1,7 +1,7 @@ // 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 { getCard, postEvent } from '../ao-lib/api.js' import { getNewHighestEchelonScore } from './priority.js' import { createCardInteractive } from './cards.js' import { promptMenu } from './welcome.js' @@ -136,16 +136,28 @@ async function subtaskCardMenu(card, index, inId, allPriorities) { const answer = await promptMenu(subtaskChoices, 'Card: ' + card.name) switch(answer) { case 'grab': - await grabCard(taskId) + await postEvent({ + type: 'task-grabbed', + taskId: taskId, + memberId: memberId + }) break case 'drop': - await dropCard(taskId) + await postEvent({ + type: 'task-dropped', + taskId: taskId, + memberId: memberId + }) break case 'check': if(isChecked) { - await uncheckCard(taskId) + await postEvent({ + type: 'task-unclaimed', + taskId: taskId, + memberId: memberId + }) } else { - await completeCard(taskId) + await postEvent({ type: 'task-claimed', taskId: taskId, memberId: memberId }) } break case 'tag': @@ -158,11 +170,21 @@ async function subtaskCardMenu(card, index, inId, allPriorities) { 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) + await postEvent({ + type: 'task-prioritized', + taskId: taskId, + inId: inId, + position: newPosition, + ...(newEchelonScore && { echelon: newEchelonScore }), + }) return false case 'downboat': console.log(taskId, inId, 'discard') - await discardCardFromCard(taskId, inId) + await postEvent({ + type: 'task-de-sub-tasked', + taskId: inId, + subTask: taskId, + }) return false case 'browse': let previousAnswer diff --git a/scripts/priority.js b/scripts/priority.js index 14fb523..58321a0 100644 --- a/scripts/priority.js +++ b/scripts/priority.js @@ -1,7 +1,7 @@ // Prioritize cards within other cards. Each card has a .priorities array of other taskIds. import { headerStyle } from './styles.js' import { aoEnv } from '../ao-lib/settings.js' -import { getCard, prioritizeCard, completeCard, uncheckCard, refocusCard } from '../ao-lib/api.js' +import { getCard, postEvent } from '../ao-lib/api.js' import { createCardInteractive } from './cards.js' import { promptMenu } from './welcome.js' @@ -148,17 +148,27 @@ async function priorityCardMenu(card, index, allPriorities) { switch(answer) { case 'check': if(isChecked) { - await uncheckCard(taskId) + await postEvent({ type: 'task-unclaimed', taskId: taskId, memberId: memberId }) } else { - await completeCard(taskId) + await postEvent({ type: 'task-claimed', taskId: taskId, memberId: memberId }) } break case 'upboat': const { newPosition, newEchelonScore } = getNewHighestEchelonScore(card.echelon, allPriorities) - await prioritizeCard(taskId, memberId, newPosition, newEchelonScore) + await postEvent({ + type: 'task-prioritized', + taskId: taskId, + inId: memberId, + position: newPosition, + ...(newEchelonScore && { echelon: newEchelonScore }), + }) return false case 'downboat': - await refocusCard(taskId, memberId) + await postEvent({ + type: 'task-refocused', + taskId: taskId, + inId: memberId, + }) return false case 'browse': break diff --git a/scripts/services.js b/scripts/services.js index 9fd8f6e..ac32570 100644 --- a/scripts/services.js +++ b/scripts/services.js @@ -7,6 +7,7 @@ import { isFile } from './files.js' import { askQuestionText } from './welcome.js' const SERVICE_FOLDER_PATH = '/etc/systemd/system' +const USER_SERVICE_FOLDER_PATH = '/usr/lib/systemd/system' // Manages a single systemctl service. serviceName is required. Implements the same functions as an AO feature module. // serviceFileText is optional but if omitted, install(), uninstall(), and isInstalledToSpec() will not exist @@ -45,6 +46,12 @@ export default class SystemServiceManager { return path.join(SERVICE_FOLDER_PATH, this.name) } + // Returns the full path to the service file that MIGHT exist in the /usr user-specific service file directory + userServicePath() { + this.assertInitialized() + return path.join(USER_SERVICE_FOLDER_PATH, this.name) + } + status(verbose = true) { if(this.isInstalled(verbose)) { if(this.isRunning(verbose)) { @@ -115,15 +122,31 @@ export default class SystemServiceManager { this.assertInitialized() console.log('Deleting service file. You may be asked for you sudo password.') this.stop() + let deleted = false try { - execSync('sudo rm ' + this.servicePath()) - if(verbose) console.log("Deleted service file.") - execSync('sudo systemctl daemon-reload') - return true + execSync('sudo rm ' + this.servicePath() + ' 2>&1') + deleted = true } catch(error) { - if(verbose) console.log('Failed to delete service file:', error) } - return false + try { + execSync('sudo rm ' + this.userServicePath() + ' 2>&1') + if(verbose) console.log(`Deleted service file in ${USER_SERVICE_FOLDER_PATH} directory.`) + deleted = true + } catch(err) {} // This one might not exist so we can ignore errors + + if(verbose && deleted) { + console.log("Deleted service file.") + } else if(verbose && !deleted) { + console.log('Failed to delete service file.') + } + + try { + execSync('sudo systemctl daemon-reload') + } catch(err) { + console.log('Failed to reload services. Most likely, there are errors in your service files.') + return false + } + return true } // Returns true if the service is currently running without errors diff --git a/scripts/shadowchat.js b/scripts/shadowchat.js index 94544c0..510e517 100644 --- a/scripts/shadowchat.js +++ b/scripts/shadowchat.js @@ -1,3 +1,4 @@ +// This file will be depracated in favor of ./chat.js // 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 // As this feature gets build, sensible standards must be developed around when tor addresses change hands, when users authenticate, etc @@ -6,8 +7,8 @@ import { isLoggedIn } from './session.js' import { startPublicBootstrap } from './bootstrap.js' import { headerStyle } from './styles.js' import { askQuestionText, promptMenu } from './welcome.js' -import { AO_DEFAULT_HOSTNAME, startSocketListeners, socketStatus, socket, shadowchat } from '../ao-lib/api.js' -import { execSync } from 'child_process' +import { AO_DEFAULT_HOSTNAME, startSocketListeners, socketStatus, socket, postEvent } from '../ao-lib/api.js' +import { spawnSync } from 'child_process' import { fileURLToPath } from 'url' import path from 'path' @@ -67,7 +68,10 @@ export default async function chatMenu() { break case 'join_chat': console.log('Launching Simplex Chat...') - execSync('bash simplex-chat') + const doNothing = () => {} + process.on('SIGINT', doNothing); + spawnSync('simplex-chat', [], { stdio: 'inherit' }) + process.removeListener('SIGINT', doNothing) break 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.') @@ -139,7 +143,12 @@ async function chatInRoom(room) { resolve(false) default: //socket.emit('chat', room, message, username || undefined) - shadowchat(room, message, username || undefined) + postEvent({ + type: 'shadowchat', + room: room, + name: username || undefined, + message: message, + }) } } while(message !== 'exit') }) diff --git a/scripts/system.js b/scripts/system.js index 008d2d5..f1996fa 100644 --- a/scripts/system.js +++ b/scripts/system.js @@ -152,7 +152,7 @@ function installPackage(packageName) { } // 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) { +export 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.') @@ -194,4 +194,4 @@ export async function isNpmPackageInstalled(path, verbose = false) { } } return false -} \ No newline at end of file +} diff --git a/scripts/tags.js b/scripts/tags.js index 1ba83c4..2b0f535 100644 --- a/scripts/tags.js +++ b/scripts/tags.js @@ -6,7 +6,8 @@ // 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' +import { getCardsForTag, getCardByName, postEvent } from '../ao-lib/api.js' +import { aoEnv } from '../ao-lib/settings.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) { @@ -21,7 +22,11 @@ export async function tagCardInteractive(card) { if(!answer || answer === 'ESC' || answer == '') { return } - await tagCard(card.taskId, answer) + await postEvent({ + type: 'task-guilded', + taskId: card.taskId, + guild: answer, + }) } // The tags index displays the tag of every card with a tag, sorted by echelon, then by we'll see! @@ -124,7 +129,13 @@ async function tagToTopEchelon(tag, allTaggedCards) { console.log('Could not find the tag you just selected, this is bad') return } - await setCardProperty(firstCardWithTag.taskId, 'echelon', highestEchelonScore) + await postEvent(firstCardWithTag.taskId, 'echelon', highestEchelonScore) + await postEvent({ + type: 'task-property-set', + taskId: firstCardWithTag.taskId, + property: 'echelon', + value: highestEchelonScore, + }) return } @@ -144,7 +155,16 @@ export async function tagTagInteractive(tag, allTaggedCards) { // Make a card for the tag if it doesn't already exist let guildCard = (await getCardByName(tag, false))[0] if(!guildCard) { - await createCard(tag) + const memberId = aoEnv('AO_CLI_SESSION_MEMBERID') + if(!memberId) { + return false + } + await postEvent({ + type: 'task-created', + name: tag, + deck: [memberId], + inId: memberId + }) guildCard = (await getCardByName(tag, false))[0] if(!guildCard) { console.log('Failed to create tag card, sorry.') diff --git a/scripts/welcome.js b/scripts/welcome.js index b3414c3..e89aad8 100644 --- a/scripts/welcome.js +++ b/scripts/welcome.js @@ -1,5 +1,4 @@ import chalk from 'chalk' -//import inquirer from 'inquirer' import prompts from 'prompts' import { selectRandom } from '../ao-lib/util.js' import { greenChalk, theAO, theMenu, headerStyle } from './styles.js' @@ -139,7 +138,9 @@ export async function promptMenu(choices, prompt = 'Please choose:', hint = '(Us } } } - console.log() + if(defaultValue && typeof defaultValue !== 'number') { + defaultValue = null + } const answer = await prompts({ type: 'select', name: 'value', @@ -155,14 +156,15 @@ export async function promptMenu(choices, prompt = 'Please choose:', hint = '(Us if(typeof answer.value === 'string') { return answer.value } - if(!isNaN(answer.value) && (answer.value < 0 || answer.value > choices.length) || answer.value === false) { - return false + if(!isNaN(answer.value) && (answer.value >= 0 && answer.value < choices.length)) { + const chosenOption = choices[answer.value]?.value || choices[answer.value]?.title || choices[answer.value] + if(isNaN(chosenOption) && !chosenOption) { + return answer.value + } + return chosenOption } else if(isNaN(answer.value)) { return answer.value } - const chosenOption = choices[answer.value].value || choices[answer.value].title - if(!chosenOption) { - return choices[answer.value] - } - return chosenOption + + return false } diff --git a/scripts/wizard.js b/scripts/wizard.js index 6519d7a..a13bdc9 100644 --- a/scripts/wizard.js +++ b/scripts/wizard.js @@ -1,13 +1,13 @@ -// 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 { execSync } from 'child_process' import { aoEnv, setAoEnv } from '../ao-lib/settings.js' -import { detectOS, updateSoftware, isInstalled } from './system.js' +import { detectOS, updateSoftware, isInstalled, installIfNotInstalled } from './system.js' import { isFolder, isFile } from './files.js' -import { aoIsInstalled } from './features/ao-server.js' +import { aoIsInstalled } from '../features/ao-server.js' +import features from '../features/index.js' import { asciiArt } from './console.js' import { headerStyle, heading2, styledStatus } from './styles.js' -import features from './features/index.js' import { yesOrNo, promptMenu } from './welcome.js' import { isNpmPackageInstalled } from './system.js' @@ -275,3 +275,34 @@ export function installRequired() { function setNodeVersion() { execSync('source ~/Alchemy/ingredients/lead && source ~/Alchemy/ingredients/iron && set_node_to v16.13.0') } + +// Checks if the AO server is running, and if not, offers to start it +export async function checkAoServerInteractive() { + const aoVersion = aoEnv('AO_VERSION') || 'ao-server' + switch(aoVersion) { + case 'ao-3': + console.log('Check for running ao-3 server not yet implemented') + return false + case 'ao-server': + case 'ao-svelte': + case 'ao-cli': + if(!features['ao-server'].submodules[0].isInstalled()) { + const response = await yesOrNo('ao-server background service is not installed. Install it now?') + if(response === true) { + features['ao-server'].submodules[0].install() + } else { + return false + } + } + if(features['ao-server'].status() === 'running') { + return true + } else { + const response = await yesOrNo('ao-server is not running. Start it now?') + if(response === true) { + features['ao-server'].submodules[0].start() + return true + } + return false + } + } +}