@dotdrelle/wiki-manager 0.6.30 → 0.6.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/wiki-manager CHANGED
@@ -1,6 +1,44 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ CACERT_PATH="${WIKI_MANAGER_CACERT_PATH:-}"
5
+ ARGS=()
6
+ while [[ $# -gt 0 ]]; do
7
+ case "$1" in
8
+ --cacert)
9
+ [[ $# -ge 2 ]] || { printf 'Error: --cacert requires a file path\n' >&2; exit 2; }
10
+ CACERT_PATH="$2"
11
+ shift 2
12
+ ;;
13
+ *)
14
+ ARGS+=("$1")
15
+ shift
16
+ ;;
17
+ esac
18
+ done
19
+
20
+ absolute_file_path() {
21
+ local value="$1"
22
+ local dir base
23
+ if [[ "$value" = /* ]]; then
24
+ printf '%s\n' "$value"
25
+ return
26
+ fi
27
+ dir="$(dirname "$value")"
28
+ base="$(basename "$value")"
29
+ printf '%s/%s\n' "$(cd "$dir" && pwd)" "$base"
30
+ }
31
+
32
+ if [[ -n "$CACERT_PATH" ]]; then
33
+ CACERT_PATH="$(absolute_file_path "$CACERT_PATH")"
34
+ [[ -f "$CACERT_PATH" ]] || { printf 'Error: --cacert file not found: %s\n' "$CACERT_PATH" >&2; exit 2; }
35
+ export WIKI_MANAGER_CACERT_PATH="$CACERT_PATH"
36
+ export NODE_EXTRA_CA_CERTS="$CACERT_PATH"
37
+ export SSL_CERT_FILE="$CACERT_PATH"
38
+ export REQUESTS_CA_BUNDLE="$CACERT_PATH"
39
+ export CURL_CA_BUNDLE="$CACERT_PATH"
40
+ fi
41
+
4
42
  BUN_BIN="$(command -v bun 2>/dev/null || true)"
5
43
  if [[ -z "$BUN_BIN" && -n "${BUN_INSTALL:-}" && -x "$BUN_INSTALL/bin/bun" ]]; then
6
44
  BUN_BIN="$BUN_INSTALL/bin/bun"
@@ -27,4 +65,4 @@ while [[ -L "$SCRIPT_SOURCE" ]]; do
27
65
  done
28
66
  BIN_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
29
67
 
30
- exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js" "$@"
68
+ exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js" "${ARGS[@]}"
@@ -1,6 +1,59 @@
1
1
  #!/usr/bin/env bun
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
2
4
 
3
- import '@opentui/solid/preload';
5
+ function valueAfter(argv, flag) {
6
+ const index = argv.indexOf(flag);
7
+ if (index === -1) return undefined;
8
+ return argv[index + 1];
9
+ }
10
+
11
+ function stripOptionWithValue(argv, flag) {
12
+ const result = [];
13
+ for (let index = 0; index < argv.length; index += 1) {
14
+ if (argv[index] === flag) {
15
+ index += 1;
16
+ continue;
17
+ }
18
+ result.push(argv[index]);
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function cacertEnvVars(cacert) {
24
+ return {
25
+ WIKI_MANAGER_CACERT_PATH: cacert,
26
+ NODE_EXTRA_CA_CERTS: cacert,
27
+ SSL_CERT_FILE: cacert,
28
+ REQUESTS_CA_BUNDLE: cacert,
29
+ CURL_CA_BUNDLE: cacert,
30
+ };
31
+ }
32
+
33
+ function resolveCacert(argv) {
34
+ const cacert = valueAfter(argv, '--cacert');
35
+ if (!cacert) return { argv, cacert: null };
36
+ const absolute = resolve(cacert);
37
+ if (!existsSync(absolute)) {
38
+ throw new Error(`--cacert file not found: ${absolute}`);
39
+ }
40
+ return {
41
+ argv: stripOptionWithValue(argv, '--cacert'),
42
+ cacert: absolute,
43
+ };
44
+ }
45
+
46
+ async function reexecWithCacertIfNeeded(argv, cacert) {
47
+ if (!cacert || process.env.WIKI_MANAGER_CACERT_BOOTSTRAPPED === '1') return;
48
+ const { spawnSync } = await import('node:child_process');
49
+ const env = { ...process.env, WIKI_MANAGER_CACERT_BOOTSTRAPPED: '1', ...cacertEnvVars(cacert) };
50
+ const result = spawnSync(process.execPath, [process.argv[1], ...argv], {
51
+ env,
52
+ stdio: 'inherit',
53
+ });
54
+ if (result.error) throw result.error;
55
+ process.exit(result.status ?? 1);
56
+ }
4
57
 
5
58
  function formatStartupError(err) {
6
59
  const message = err instanceof Error ? err.message : String(err);
@@ -21,7 +74,13 @@ function formatStartupError(err) {
21
74
  }
22
75
 
23
76
  async function main() {
24
- const argv = process.argv.slice(2);
77
+ const parsed = resolveCacert(process.argv.slice(2));
78
+ const argv = parsed.argv;
79
+ await reexecWithCacertIfNeeded(argv, parsed.cacert);
80
+ // Fallback for already-bootstrapped direct invocations; the shell wrapper
81
+ // exports these before Bun starts.
82
+ if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
83
+ await import('@opentui/solid/preload');
25
84
  const interactive = process.stdout.isTTY && process.stdin.isTTY && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
26
85
  if (interactive) process.stdout.write('Starting wiki-manager…\r');
27
86
  const { runCli } = await import('../src/cli/wiki-manager.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.6.30",
3
+ "version": "0.6.31",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "author": "dotrelle",
@@ -562,6 +562,7 @@ Usage:
562
562
  Options:
563
563
  -v, --version Print version
564
564
  -h, --help Print help
565
+ --cacert <path> Trust a local CA; Docker must be able to read this host path
565
566
  --once <prompt> Run one agent turn and exit
566
567
  --headless Run a workspace task non-interactively
567
568
  --workspace <name> Workspace for --headless
@@ -0,0 +1,66 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import YAML from 'yaml';
4
+ import { managerRuntimeDir } from './env.js';
5
+
6
+ export const CACERT_CONTAINER_PATH = '/wiki-manager-ca.pem';
7
+ export const CACERT_ENV_KEYS = [
8
+ 'NODE_EXTRA_CA_CERTS',
9
+ 'SSL_CERT_FILE',
10
+ 'REQUESTS_CA_BUNDLE',
11
+ 'CURL_CA_BUNDLE',
12
+ ];
13
+
14
+ export function activeCacertPath() {
15
+ const value = process.env.WIKI_MANAGER_CACERT_PATH;
16
+ return value ? resolve(value) : null;
17
+ }
18
+
19
+ export function cacertEnv(path = activeCacertPath(), targetPath = path) {
20
+ if (!path) return {};
21
+ return Object.fromEntries(CACERT_ENV_KEYS.map((key) => [key, targetPath]));
22
+ }
23
+
24
+ function parseComposeServices(composeFilePath) {
25
+ const parsed = YAML.parse(readFileSync(composeFilePath, 'utf8')) ?? {};
26
+ return Object.keys(parsed.services ?? {}).filter(Boolean);
27
+ }
28
+
29
+ function composeOverrideContent(cacertPath, services) {
30
+ const serviceEntries = Object.fromEntries(
31
+ services.map((service) => [
32
+ service,
33
+ {
34
+ volumes: [`${cacertPath}:${CACERT_CONTAINER_PATH}:ro`],
35
+ environment: cacertEnv(cacertPath, CACERT_CONTAINER_PATH),
36
+ },
37
+ ]),
38
+ );
39
+ const doc = {
40
+ services: serviceEntries,
41
+ };
42
+ return [
43
+ '# Generated by wiki-manager. Safe to delete.',
44
+ '# Rewritten when --cacert is used with a Docker Compose command.',
45
+ YAML.stringify(doc).trimEnd(),
46
+ '',
47
+ ].join('\n');
48
+ }
49
+
50
+ let _cacertOverrideCache;
51
+
52
+ export function ensureCacertComposeOverride(composeFilePath, fileName = 'cacert.compose.yml') {
53
+ if (_cacertOverrideCache !== undefined) return _cacertOverrideCache;
54
+ const cacertPath = activeCacertPath();
55
+ if (!cacertPath) return (_cacertOverrideCache = null);
56
+ if (!existsSync(cacertPath)) {
57
+ throw new Error(`--cacert file not found: ${cacertPath}`);
58
+ }
59
+ const services = parseComposeServices(composeFilePath);
60
+ if (services.length === 0) return null;
61
+ const runtimeDir = managerRuntimeDir();
62
+ mkdirSync(runtimeDir, { recursive: true });
63
+ const overridePath = join(runtimeDir, fileName);
64
+ writeFileSync(overridePath, composeOverrideContent(cacertPath, services), 'utf8');
65
+ return (_cacertOverrideCache = overridePath);
66
+ }
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  import YAML from 'yaml';
6
+ import { cacertEnv, ensureCacertComposeOverride } from './cacert.js';
6
7
  import { managerEnvFile, readEnvFile } from './env.js';
7
8
  import { managerRoot } from './workspaces.js';
8
9
 
@@ -93,14 +94,14 @@ function projectName(session) {
93
94
  }
94
95
 
95
96
  function composeBaseArgs(session) {
96
- const args = ['compose', '-f', composeFile(), '-p', projectName(session)];
97
+ const compose = composeFile();
98
+ const cacertOverride = ensureCacertComposeOverride(compose);
99
+ const args = ['compose', '-f', compose];
100
+ if (cacertOverride) args.push('-f', cacertOverride);
101
+ args.push('-p', projectName(session));
97
102
  const managerEnvPath = managerEnvFile();
98
- if (existsSync(managerEnvPath)) {
99
- args.push('--env-file', managerEnvPath);
100
- }
101
- if (session.workspaceEnvFile && existsSync(session.workspaceEnvFile)) {
102
- args.push('--env-file', session.workspaceEnvFile);
103
- }
103
+ if (existsSync(managerEnvPath)) args.push('--env-file', managerEnvPath);
104
+ if (session.workspaceEnvFile && existsSync(session.workspaceEnvFile)) args.push('--env-file', session.workspaceEnvFile);
104
105
  return args;
105
106
  }
106
107
 
@@ -109,6 +110,7 @@ function composeEnv(session) {
109
110
  ...process.env,
110
111
  ...readManagerEnv(),
111
112
  ...(session.workspaceEnv ?? {}),
113
+ ...cacertEnv(),
112
114
  WORKSPACE_NAME: session.workspace,
113
115
  WIKI_WORKSPACE_PATH: session.workspacePath,
114
116
  };
package/src/core/env.js CHANGED
@@ -1,18 +1,28 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
2
+ import { dirname, join, resolve } from 'node:path';
3
3
 
4
4
  export function userManagerDir() {
5
5
  return process.cwd();
6
6
  }
7
7
 
8
+ export function managerStateDir() {
9
+ return process.env.WIKI_MANAGER_ENV_FILE
10
+ ? dirname(resolve(process.env.WIKI_MANAGER_ENV_FILE))
11
+ : userManagerDir();
12
+ }
13
+
14
+ export function managerRuntimeDir() {
15
+ return join(managerStateDir(), '.wiki-manager');
16
+ }
17
+
8
18
  export function managerEnvFile() {
9
19
  return process.env.WIKI_MANAGER_ENV_FILE
10
20
  ? resolve(process.env.WIKI_MANAGER_ENV_FILE)
11
- : join(userManagerDir(), '.env');
21
+ : join(managerStateDir(), '.env');
12
22
  }
13
23
 
14
24
  export function managerMcpEndpointsFile() {
15
- return join(userManagerDir(), 'mcp.endpoints.json');
25
+ return join(managerStateDir(), 'mcp.endpoints.json');
16
26
  }
17
27
 
18
28
  function parseEnvValue(value) {
package/src/core/mcp.js CHANGED
@@ -211,7 +211,7 @@ async function mcpRequest(endpoint, method, params, signal, options = {}) {
211
211
  params: {
212
212
  protocolVersion: '2025-06-18',
213
213
  capabilities: {},
214
- clientInfo: { name: 'wiki-manager', version: '0.6.30' },
214
+ clientInfo: { name: 'wiki-manager', version: '0.6.31' },
215
215
  },
216
216
  }),
217
217
  });
package/wiki-workspace CHANGED
@@ -16,10 +16,66 @@ DEFAULT_MANAGER_DIR="$PWD"
16
16
  WORKSPACES_DIR="${WIKI_WORKSPACES_DIR:-$DEFAULT_MANAGER_DIR/workspaces}"
17
17
  MANAGER_ENV_FILE="${WIKI_MANAGER_ENV_FILE:-$DEFAULT_MANAGER_DIR/.env}"
18
18
  MANAGER_ENDPOINTS_FILE="${WIKI_MANAGER_ENDPOINTS_FILE:-$DEFAULT_MANAGER_DIR/mcp.endpoints.json}"
19
+ MANAGER_STATE_DIR="$(cd "$(dirname "$MANAGER_ENV_FILE")" && pwd)"
20
+ MANAGER_RUNTIME_DIR="$MANAGER_STATE_DIR/.wiki-manager"
21
+ CACERT_PATH="${WIKI_MANAGER_CACERT_PATH:-}"
22
+
23
+ normalize_path() {
24
+ local value="$1"
25
+ if command -v wslpath >/dev/null 2>&1 && [[ "$value" =~ ^[A-Za-z]:[\\/] ]]; then
26
+ wslpath -u "$value"
27
+ return
28
+ fi
29
+ printf '%s\n' "$value"
30
+ }
31
+
32
+ absolute_path() {
33
+ local value="$1"
34
+ local base="${2:-$ROOT_DIR}"
35
+ value="$(normalize_path "$value")"
36
+ if [[ "$value" = /* ]]; then
37
+ printf '%s\n' "$value"
38
+ else
39
+ printf '%s\n' "$base/$value"
40
+ fi
41
+ }
42
+
43
+ parse_global_options() {
44
+ local parsed=()
45
+ while [[ $# -gt 0 ]]; do
46
+ case "$1" in
47
+ --cacert)
48
+ [[ $# -ge 2 ]] || { printf 'Error: --cacert requires a file path\n' >&2; exit 2; }
49
+ CACERT_PATH="$2"
50
+ shift 2
51
+ ;;
52
+ *)
53
+ parsed+=("$1")
54
+ shift
55
+ ;;
56
+ esac
57
+ done
58
+ if [[ -n "$CACERT_PATH" ]]; then
59
+ CACERT_PATH="$(absolute_path "$CACERT_PATH" "$PWD")"
60
+ [[ -f "$CACERT_PATH" ]] || { printf 'Error: --cacert file not found: %s\n' "$CACERT_PATH" >&2; exit 2; }
61
+ export WIKI_MANAGER_CACERT_PATH="$CACERT_PATH"
62
+ export NODE_EXTRA_CA_CERTS="$CACERT_PATH"
63
+ export SSL_CERT_FILE="$CACERT_PATH"
64
+ export REQUESTS_CA_BUNDLE="$CACERT_PATH"
65
+ export CURL_CA_BUNDLE="$CACERT_PATH"
66
+ fi
67
+ set -- "${parsed[@]}"
68
+ PARSED_ARGS=("$@")
69
+ }
70
+
71
+ PARSED_ARGS=()
72
+ parse_global_options "$@"
73
+ set -- "${PARSED_ARGS[@]}"
19
74
 
20
75
  usage() {
21
76
  cat <<'EOF'
22
77
  Usage:
78
+ wiki-workspace [--cacert <path>] <command> ...
23
79
  wiki-workspace config <workspace> [path]
24
80
  wiki-workspace up <workspace>
25
81
  wiki-workspace wiki <workspace> <command> [args...]
@@ -50,6 +106,7 @@ Commands:
50
106
  Configuration:
51
107
  workspaces/<workspace>/.env
52
108
  Override directory: WIKI_WORKSPACES_DIR=/path/to/dir
109
+ Local CA: --cacert /absolute/path/to/ca.pem (Docker must be able to read this host path)
53
110
  EOF
54
111
  }
55
112
 
@@ -58,26 +115,6 @@ die() {
58
115
  exit 1
59
116
  }
60
117
 
61
- normalize_path() {
62
- local value="$1"
63
- if command -v wslpath >/dev/null 2>&1 && [[ "$value" =~ ^[A-Za-z]:[\\/] ]]; then
64
- wslpath -u "$value"
65
- return
66
- fi
67
- printf '%s\n' "$value"
68
- }
69
-
70
- absolute_path() {
71
- local value="$1"
72
- local base="${2:-$ROOT_DIR}"
73
- value="$(normalize_path "$value")"
74
- if [[ "$value" = /* ]]; then
75
- printf '%s\n' "$value"
76
- else
77
- printf '%s\n' "$base/$value"
78
- fi
79
- }
80
-
81
118
  workspace_env_file() {
82
119
  local workspace="$1"
83
120
  printf '%s/%s/.env\n' "$WORKSPACES_DIR" "$workspace"
@@ -169,9 +206,12 @@ agents_compose() {
169
206
  [[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
170
207
 
171
208
  _agents_dc() {
209
+ local cacert_args=()
210
+ WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
211
+ read_lines_into_array cacert_args cacert_compose_args "$agents_compose_file" "agents.cacert.compose.yml"
172
212
  WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
173
213
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
174
- "${compose_env_args[@]}" -f "$agents_compose_file" -p wiki-agents "$@"
214
+ "${compose_env_args[@]}" -f "$agents_compose_file" "${cacert_args[@]}" -p wiki-agents "$@"
175
215
  }
176
216
 
177
217
  case "$subcommand" in
@@ -444,13 +484,19 @@ compose_for_workspace() {
444
484
  local compose_env_args=()
445
485
  [[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
446
486
  compose_env_args+=(--env-file "$workspace_env")
487
+ local cacert_args=()
488
+ WIKI_WORKSPACE_PATH="$ws_path" \
489
+ WIKI_SERVE_PORT="$serve_port" \
490
+ WIKI_MCP_PORT="$mcp_port" \
491
+ PRODUCTION_MCP_PORT="$prod_port" \
492
+ read_lines_into_array cacert_args cacert_compose_args "$ROOT_DIR/docker-compose.yml" "cacert.compose.yml"
447
493
 
448
494
  WIKI_WORKSPACE_PATH="$ws_path" \
449
495
  WIKI_SERVE_PORT="$serve_port" \
450
496
  WIKI_MCP_PORT="$mcp_port" \
451
497
  PRODUCTION_MCP_PORT="$prod_port" \
452
498
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
453
- "${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" -p "$project" "$@"
499
+ "${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" "${cacert_args[@]}" -p "$project" "$@"
454
500
  }
455
501
 
456
502
  logs_args() {
@@ -474,6 +520,48 @@ read_lines_into_array() {
474
520
  done < <("$@")
475
521
  }
476
522
 
523
+ cacert_compose_args() {
524
+ local compose_file="$1"
525
+ local override_name="$2"
526
+ [[ -n "$CACERT_PATH" ]] || return 0
527
+ command -v node >/dev/null 2>&1 || die "--cacert requires node to generate Docker Compose overrides"
528
+
529
+ local services
530
+ services="$(
531
+ ROOT_DIR="$ROOT_DIR" _WIKI_COMPOSE_FILE_PATH="$compose_file" node <<'NODE'
532
+ const { createRequire } = require('node:module');
533
+ const fs = require('node:fs');
534
+ const requireFromPackage = createRequire(`${process.env.ROOT_DIR}/package.json`);
535
+ const YAML = requireFromPackage('yaml');
536
+ const parsed = YAML.parse(fs.readFileSync(process.env._WIKI_COMPOSE_FILE_PATH, 'utf8')) ?? {};
537
+ for (const service of Object.keys(parsed.services ?? {})) {
538
+ console.log(service);
539
+ }
540
+ NODE
541
+ )"
542
+ [[ -n "$services" ]] || return 0
543
+
544
+ mkdir -p "$MANAGER_RUNTIME_DIR"
545
+ local override_path="$MANAGER_RUNTIME_DIR/$override_name"
546
+ {
547
+ printf '# Generated by wiki-manager. Safe to delete.\n'
548
+ printf '# Rewritten when --cacert is used with a Docker Compose command.\n'
549
+ printf 'services:\n'
550
+ while IFS= read -r service || [[ -n "$service" ]]; do
551
+ [[ -n "$service" ]] || continue
552
+ printf ' %s:\n' "$service"
553
+ printf ' volumes:\n'
554
+ printf ' - %s:/wiki-manager-ca.pem:ro\n' "$CACERT_PATH"
555
+ printf ' environment:\n'
556
+ printf ' NODE_EXTRA_CA_CERTS: /wiki-manager-ca.pem\n'
557
+ printf ' SSL_CERT_FILE: /wiki-manager-ca.pem\n'
558
+ printf ' REQUESTS_CA_BUNDLE: /wiki-manager-ca.pem\n'
559
+ printf ' CURL_CA_BUNDLE: /wiki-manager-ca.pem\n'
560
+ done <<< "$services"
561
+ } > "$override_path"
562
+ printf '%s\n' -f "$override_path"
563
+ }
564
+
477
565
  run_wiki() {
478
566
  local workspace="$1"
479
567
  shift
@@ -623,12 +711,17 @@ config_workspace() {
623
711
  local compose_env_args=()
624
712
  [[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
625
713
  compose_env_args+=(--env-file "$env_file")
714
+ local cacert_args=()
715
+ WIKI_WORKSPACE_PATH="$target_path" \
716
+ WIKI_SERVE_PORT="$serve_port" \
717
+ WIKI_MCP_PORT="$mcp_port" \
718
+ read_lines_into_array cacert_args cacert_compose_args "$ROOT_DIR/docker-compose.yml" "cacert.compose.yml"
626
719
 
627
720
  WIKI_WORKSPACE_PATH="$target_path" \
628
721
  WIKI_SERVE_PORT="$serve_port" \
629
722
  WIKI_MCP_PORT="$mcp_port" \
630
723
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
631
- "${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" run --rm wiki init
724
+ "${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" "${cacert_args[@]}" run --rm wiki init
632
725
 
633
726
  printf 'Workspace ready: %s\n' "$target_path"
634
727
  printf 'Start with: wiki-workspace up %s\n' "$workspace"