Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,21 +487,24 @@ EXAMPLES

## `bitsocial challenge remove NAME`

Remove an installed challenge package
Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force is passed

```
USAGE
$ bitsocial challenge remove NAME --pkcRpcUrl <value> [--pkcOptions.dataPath <value>]
$ bitsocial challenge remove NAME --pkcRpcUrl <value> [--pkcOptions.dataPath <value>] [--force]

ARGUMENTS
NAME The challenge package name (e.g., my-challenge or @scope/my-challenge)

FLAGS
--force Remove without checking whether communities still use it (the daemon is not contacted
for the check)
--pkcOptions.dataPath=<value> Data path where challenges are installed
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC

DESCRIPTION
Remove an installed challenge package
Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force
is passed

ALIASES
$ bitsocial challenge uninstall
Expand All @@ -512,27 +515,32 @@ EXAMPLES
$ bitsocial challenge remove my-challenge

$ bitsocial challenge remove @scope/my-challenge

$ bitsocial challenge remove my-challenge --force
```

_See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.108/src/cli/commands/challenge/remove.ts)_

## `bitsocial challenge rm NAME`

Remove an installed challenge package
Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force is passed

```
USAGE
$ bitsocial challenge rm NAME --pkcRpcUrl <value> [--pkcOptions.dataPath <value>]
$ bitsocial challenge rm NAME --pkcRpcUrl <value> [--pkcOptions.dataPath <value>] [--force]

ARGUMENTS
NAME The challenge package name (e.g., my-challenge or @scope/my-challenge)

FLAGS
--force Remove without checking whether communities still use it (the daemon is not contacted
for the check)
--pkcOptions.dataPath=<value> Data path where challenges are installed
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC

DESCRIPTION
Remove an installed challenge package
Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force
is passed

ALIASES
$ bitsocial challenge uninstall
Expand All @@ -543,25 +551,30 @@ EXAMPLES
$ bitsocial challenge remove my-challenge

$ bitsocial challenge remove @scope/my-challenge

$ bitsocial challenge remove my-challenge --force
```

## `bitsocial challenge un NAME`

Remove an installed challenge package
Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force is passed

```
USAGE
$ bitsocial challenge un NAME --pkcRpcUrl <value> [--pkcOptions.dataPath <value>]
$ bitsocial challenge un NAME --pkcRpcUrl <value> [--pkcOptions.dataPath <value>] [--force]

ARGUMENTS
NAME The challenge package name (e.g., my-challenge or @scope/my-challenge)

FLAGS
--force Remove without checking whether communities still use it (the daemon is not contacted
for the check)
--pkcOptions.dataPath=<value> Data path where challenges are installed
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC

DESCRIPTION
Remove an installed challenge package
Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force
is passed

ALIASES
$ bitsocial challenge uninstall
Expand All @@ -572,25 +585,30 @@ EXAMPLES
$ bitsocial challenge remove my-challenge

$ bitsocial challenge remove @scope/my-challenge

$ bitsocial challenge remove my-challenge --force
```

## `bitsocial challenge uninstall NAME`

Remove an installed challenge package
Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force is passed

```
USAGE
$ bitsocial challenge uninstall NAME --pkcRpcUrl <value> [--pkcOptions.dataPath <value>]
$ bitsocial challenge uninstall NAME --pkcRpcUrl <value> [--pkcOptions.dataPath <value>] [--force]

ARGUMENTS
NAME The challenge package name (e.g., my-challenge or @scope/my-challenge)

FLAGS
--force Remove without checking whether communities still use it (the daemon is not contacted
for the check)
--pkcOptions.dataPath=<value> Data path where challenges are installed
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC

DESCRIPTION
Remove an installed challenge package
Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force
is passed

ALIASES
$ bitsocial challenge uninstall
Expand All @@ -601,6 +619,8 @@ EXAMPLES
$ bitsocial challenge remove my-challenge

$ bitsocial challenge remove @scope/my-challenge

$ bitsocial challenge remove my-challenge --force
```

## `bitsocial community create`
Expand Down Expand Up @@ -1160,7 +1180,8 @@ ARGUMENTS
NAME The page sort package name (e.g., my-page-sort or @scope/my-page-sort)

FLAGS
--force Remove even if communities still use it, or if the daemon cannot be reached to check
--force Remove without checking whether communities still use it (the daemon is not contacted
for the check)
--pkcOptions.dataPath=<value> Data path where page sorts are installed
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC

Expand Down Expand Up @@ -1193,7 +1214,8 @@ ARGUMENTS
NAME The page sort package name (e.g., my-page-sort or @scope/my-page-sort)

FLAGS
--force Remove even if communities still use it, or if the daemon cannot be reached to check
--force Remove without checking whether communities still use it (the daemon is not contacted
for the check)
--pkcOptions.dataPath=<value> Data path where page sorts are installed
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC

Expand Down Expand Up @@ -1224,7 +1246,8 @@ ARGUMENTS
NAME The page sort package name (e.g., my-page-sort or @scope/my-page-sort)

FLAGS
--force Remove even if communities still use it, or if the daemon cannot be reached to check
--force Remove without checking whether communities still use it (the daemon is not contacted
for the check)
--pkcOptions.dataPath=<value> Data path where page sorts are installed
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC

Expand Down Expand Up @@ -1255,7 +1278,8 @@ ARGUMENTS
NAME The page sort package name (e.g., my-page-sort or @scope/my-page-sort)

FLAGS
--force Remove even if communities still use it, or if the daemon cannot be reached to check
--force Remove without checking whether communities still use it (the daemon is not contacted
for the check)
--pkcOptions.dataPath=<value> Data path where page sorts are installed
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC

Expand Down
24 changes: 22 additions & 2 deletions docs/challenges.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ bitsocial challenge install https://example.com/my-challenge-1.0.0.tar.gz
bitsocial challenge install ./my-local-challenge
```

All three commands take `--pkcOptions.dataPath` (default: the platform data dir from `env-paths("bitsocial")`, e.g. `~/.local/share/bitsocial` on Linux). `install` and `remove` also take `--pkcRpcUrl` (default `ws://localhost:9138`) to find the daemon to reload.
All three commands take `--pkcOptions.dataPath` (default: the platform data dir from `env-paths("bitsocial")`, e.g. `~/.local/share/bitsocial` on Linux). `install` and `remove` also take `--pkcRpcUrl` (default `ws://localhost:9138`) to find the daemon to reload (and, for `remove`, to check which communities use the package).

`remove` takes the package name, not the spec you installed from: `bitsocial challenge remove @bitsocial/mintpass-challenge`.

Expand Down Expand Up @@ -93,7 +93,27 @@ For larger changes, write the settings to a file and pass `--jsonFile <file>`; C

## Removing

`challenge remove` deletes the directory and reloads the daemon. It does **not** check whether a community still names the package in `settings.challenges`. Check first with `bitsocial community get <address>`. (`page-sort remove` does refuse in that case; see [page-sorts.md](page-sorts.md#removing).)
```bash
bitsocial challenge remove my-challenge
```

`remove` first checks that the package is installed, without contacting the daemon. Then it connects to the daemon at `--pkcRpcUrl` and checks `settings.challenges` in every community, running or stopped. If any entry names the package, it refuses and lists them (oclif prefixes each output line with ` › `):

```
Error: Challenge "my-challenge" is still used by:
- mycommunity.bso settings.challenges[1]
Change or remove those entries first, e.g. bitsocial community edit <address> --jsonFile <file with the new settings.challenges>,
or pass --force to remove it anyway (those communities reject every publication and fail to start until their settings are fixed).
```

Otherwise it deletes the directory and reloads the daemon.

The reason: pkc-js can't load a challenge whose name is no longer registered. A running community keeps advertising the old challenge but rejects every publication (`One of the community challenges is misconfigured`), and on its next start (including a daemon restart) `start()` throws and the community stays offline until its settings are changed.

- **Only `name` entries count.** An entry with a `path` loads that file, so it doesn't use the package.
- **A package named after a pkc-js built-in** (`question`, `text-math`, `fail`, `blacklist`, `whitelist`, `publication-match`) is refused the same way. Removing it hands the built-in back, so nothing breaks, but those communities switch to the built-in challenge. The refusal says so.
- **The daemon must be running** for the check. If it can't be reached, `remove` refuses and says so.
- **`--force`** skips the check and doesn't contact the daemon for it. After removing, it prints a warning that any community still naming the package rejects every publication and fails to start (or, for a built-in name, that it now uses the built-in).

## Troubleshooting

Expand Down
4 changes: 2 additions & 2 deletions docs/page-sorts.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Quote the flags in the shell: the `[0]` brackets are glob characters, and zsh re
bitsocial page-sort remove @pkcprotocol/active-page-sort
```

Before deleting, `remove` connects to the daemon at `--pkcRpcUrl` and checks every community's `settings.pages`. If any entry names the package, it refuses and lists them (oclif prefixes each output line with ` › `):
`remove` first checks that the package is installed, without contacting the daemon. Then it connects to the daemon at `--pkcRpcUrl` and checks every community's `settings.pages`. If any entry names the package, it refuses and lists them (oclif prefixes each output line with ` › `):

```
Error: Page sort "@pkcprotocol/active-page-sort" is still used by:
Expand All @@ -111,7 +111,7 @@ or pass --force to remove it anyway (the communities keep publishing without thi
The reason: a community whose `settings.pages` names a missing sort keeps publishing but skips that entry and logs an error on every start, so the board silently loses the sort.

- **The daemon must be running** for the check. If it cannot be reached, `remove` refuses and says so.
- **`--force`** skips the check, both for in-use packages and for an unreachable daemon.
- **`--force`** skips the check and doesn't contact the daemon for it. After removing, it prints a warning that any community still naming the package keeps publishing without this sort.

## Troubleshooting

Expand Down
9 changes: 9 additions & 0 deletions src/challenge-packages/challenge-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,12 @@ export const hashChallengePackageContents = hashPackageContents;

export const loadChallengesIntoPKC = (dataPath?: string): Promise<InstalledChallenge[]> =>
loadPackagesIntoPKC(CHALLENGE_PACKAGE_KIND, dataPath);

/** Where a community's settings.challenges names `challengeName`, e.g. "challenges[1]". An entry with a path loads that file instead. */
export function findChallengeUsages(challengeSettings: { name?: string; path?: string }[] | undefined, challengeName: string): string[] {
const usages: string[] = [];
challengeSettings?.forEach((entry, index) => {
if (entry?.name === challengeName && !entry.path) usages.push(`challenges[${index}]`);
});
return usages;
}
63 changes: 59 additions & 4 deletions src/cli/commands/challenge/remove.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { Args, Flags } from "@oclif/core";
import { BaseCommand } from "../../base-command.js";
import defaults from "../../../common-utils/defaults.js";
import { removePackage, PackageNotInstalledError, InvalidPackageNameError } from "../../../common-utils/package-registry.js";
import { CHALLENGE_PACKAGE_KIND, reloadChallengesInDaemon } from "../../../challenge-packages/challenge-utils.js";
import {
removePackage,
assertPackageInstalled,
collectPackageUsages,
isBuiltInPackageName,
PackageNotInstalledError,
InvalidPackageNameError,
assertValidPackageName
} from "../../../common-utils/package-registry.js";
import { CHALLENGE_PACKAGE_KIND, findChallengeUsages, reloadChallengesInDaemon } from "../../../challenge-packages/challenge-utils.js";

export default class Remove extends BaseCommand {
static override description = "Remove an installed challenge package";
static override description =
"Remove an installed challenge package. Refuses while a community's settings.challenges still names it, unless --force is passed";

static override aliases = ["challenge:uninstall", "challenge:rm", "challenge:un"];

Expand All @@ -20,30 +29,76 @@ export default class Remove extends BaseCommand {
"pkcOptions.dataPath": Flags.directory({
description: "Data path where challenges are installed",
required: false
}),
force: Flags.boolean({
description: "Remove without checking whether communities still use it (the daemon is not contacted for the check)",
default: false
})
};

static override examples = [
"bitsocial challenge remove my-challenge",
"bitsocial challenge remove @scope/my-challenge"
"bitsocial challenge remove @scope/my-challenge",
"bitsocial challenge remove my-challenge --force"
];

async run(): Promise<void> {
const { args, flags } = await this.parse(Remove);
const dataPath = flags["pkcOptions.dataPath"] || defaults.PKC_DATA_PATH;

let removed: { version?: string };
let builtIn: boolean;
try {
assertValidPackageName(args.name);
// Checked before the daemon, so a misspelled name says so rather than that the daemon is down
await assertPackageInstalled(CHALLENGE_PACKAGE_KIND, { name: args.name, dataPath });
builtIn = await isBuiltInPackageName(CHALLENGE_PACKAGE_KIND, args.name);
if (!flags.force) await this._refuseIfInUse(args.name, builtIn, flags.pkcRpcUrl.toString());
removed = await removePackage(CHALLENGE_PACKAGE_KIND, { name: args.name, dataPath });
} catch (err) {
if (err instanceof PackageNotInstalledError || err instanceof InvalidPackageNameError) this.error(err.message);
throw err;
}

this.log(`removed ${args.name}${removed.version ? `@${removed.version}` : ""}`);
if (flags.force)
this.warn(
`Skipped the in-use check. A community whose settings.challenges still names "${args.name}" ` +
(builtIn
? `now uses the pkc-js built-in challenge of the same name.`
: `rejects every publication and fails to start until that entry is changed.`)
);

// Best-effort reload in the daemon at --pkcRpcUrl, so the removal takes effect
// without a restart
await reloadChallengesInDaemon(flags.pkcRpcUrl);
}

// A community whose settings.challenges names an unregistered challenge rejects every publication, and pkc-js
// throws on its next start, so it goes offline (issue #176). A name that shadows a pkc-js built-in falls back to
// the built-in instead, which still changes how the community moderates. Ask the daemon which communities use it.
private async _refuseIfInUse(name: string, builtIn: boolean, pkcRpcUrl: string): Promise<void> {
let pkc: Awaited<ReturnType<typeof this._connectToPkcRpc>>;
try {
pkc = await this._connectToPkcRpc(pkcRpcUrl);
} catch (err) {
this.error(
`Could not check whether any community uses "${name}": ${err instanceof Error ? err.message : String(err)}\n` +
`Start the daemon and retry, or pass --force to remove it without checking.`
);
}
const usages = await collectPackageUsages(pkc, (settings) =>
findChallengeUsages(settings?.challenges, name).map((usage) => `settings.${usage}`)
);
if (usages.length === 0) return;
this.error(
`Challenge "${name}" is still used by:\n` +
usages.map((usage) => ` - ${usage}`).join("\n") +
`\nChange or remove those entries first, e.g. bitsocial community edit <address> --jsonFile <file with the new settings.challenges>,` +
` or pass --force to remove it anyway ` +
(builtIn
? `(after removal, "${name}" falls back to the pkc-js built-in challenge of the same name).`
: `(those communities reject every publication and fail to start until their settings are fixed).`)
);
}
}
Loading
Loading