Browse Source

refactored menus a bit, simplex-chat launches if installed

main
deicidus 2 years ago
parent
commit
ac13582d19
  1. 45
      ao-lib/api.js
  2. 6
      index.js
  3. 17
      package-lock.json
  4. 1
      package.json
  5. 14
      scripts/ao.js
  6. 6
      scripts/bootstrap.js
  7. 11
      scripts/cards.js
  8. 10
      scripts/connect.js
  9. 43
      scripts/features/alchemy.js
  10. 140
      scripts/features/ao-cli.js
  11. 96
      scripts/features/ao-server.js
  12. 20
      scripts/features/bitcoin.js
  13. 7
      scripts/features/borg.js
  14. 5
      scripts/features/certbot.js
  15. 5
      scripts/features/encryption.js
  16. 5
      scripts/features/files.js
  17. 5
      scripts/features/glossary.js
  18. 197
      scripts/features/index.js
  19. 5
      scripts/features/jitsi.js
  20. 5
      scripts/features/jubilee.js
  21. 23
      scripts/features/lightning.js
  22. 184
      scripts/features/manual.js
  23. 28
      scripts/features/nginx.js
  24. 5
      scripts/features/signal.js
  25. 5
      scripts/features/themes.js
  26. 25
      scripts/features/tor.js
  27. 4
      scripts/features/youtube-dl.js
  28. 36
      scripts/hand.js
  29. 20
      scripts/priority.js
  30. 35
      scripts/services.js
  31. 17
      scripts/shadowchat.js
  32. 4
      scripts/system.js
  33. 28
      scripts/tags.js
  34. 20
      scripts/welcome.js
  35. 39
      scripts/wizard.js

45
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(
() => {

6
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)) }

17
package-lock.json generated

@ -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",

1
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",

14
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) {

6
scripts/bootstrap.js vendored

@ -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.

11
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
}

10
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
}

43
scripts/features/alchemy.js

@ -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' }
]
}

140
scripts/features/ao-cli.js

@ -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' },
]
}

96
scripts/features/ao-server.js

@ -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 ]
}

20
scripts/features/bitcoin.js

@ -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
}

7
scripts/features/borg.js

@ -1,7 +0,0 @@
// BorgBackup module
export default {
name: 'Borg',
description: 'encrypted-in-transit, deduplicated incremental backup (over tor)',
status: () => null,
}

5
scripts/features/certbot.js

@ -1,5 +0,0 @@
export default {
name: 'SSL/Certbot',
description: 'HTTPS for public web AO',
status: () => null,
}

5
scripts/features/encryption.js

@ -1,5 +0,0 @@
export default {
name: 'Encryption',
description: 'serverside secret messages', //encrypt messages to and from this computer',
status: () => null,
}

5
scripts/features/files.js

@ -1,5 +0,0 @@
export default {
name: 'File hosting',
description: 'file attachments on cards (sync p2p via tor with other AOs)',
status: () => null,
}

5
scripts/features/glossary.js

@ -1,5 +0,0 @@
export default {
name: 'Glossary',
description: 'custom glossary',
status: () => null,
}

197
scripts/features/index.js

@ -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
}

5
scripts/features/jitsi.js

@ -1,5 +0,0 @@
export default {
name: 'Jitsi',
description: 'secure video chat',
status: () => null,
}

5
scripts/features/jubilee.js

@ -1,5 +0,0 @@
export default {
name: 'Jubilee',
description: 'monthly points creation event',
status: () => null,
}

23
scripts/features/lightning.js

@ -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
}

184
scripts/features/manual.js

@ -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
}

28
scripts/features/nginx.js

@ -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
}

5
scripts/features/signal.js

@ -1,5 +0,0 @@
export default {
name: 'Signal',
description: 'secure notifications',
status: () => null,
}

5
scripts/features/themes.js

@ -1,5 +0,0 @@
export default {
name: 'Themes',
description: 'custom themes',
status: () => null,
}

25
scripts/features/tor.js

@ -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
}

4
scripts/features/youtube-dl.js

@ -1,4 +0,0 @@
export default {
description: 'cache web videos',
status: () => null,
}

36
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

20
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 }),