@dotdrelle/wiki-manager 0.6.30 → 0.6.34

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.
@@ -24,14 +24,15 @@ export function SlashDialog(props: { context: any }) {
24
24
  {(item: any, index) => (
25
25
  <text
26
26
  height={1}
27
- fg={index() === context().selected ? '#111318' : '#D6DEE8'}
28
- bg={index() === context().selected ? '#8BD5CA' : '#111318'}
27
+ fg={index() === context().visibleSelected ? '#111318' : '#D6DEE8'}
28
+ bg={index() === context().visibleSelected ? '#8BD5CA' : '#111318'}
29
29
  >
30
30
  {item.value.padEnd(16, ' ')} {item.description}
31
31
  </text>
32
32
  )}
33
33
  </For>
34
- <text fg="#7F8C8D">[up/down navigate Tab complete Enter confirm Esc clear]</text>
34
+ <text height={1} />
35
+ <text fg="#7F8C8D">[up/down navigate, Tab complete, Enter confirm, Esc clear]</text>
35
36
  </box>
36
37
  )}
37
38
  </Show>
package/src/shell/repl.js CHANGED
@@ -54,8 +54,8 @@ const COMMAND_COMPLETION_DESCRIPTIONS = {
54
54
  '/help': 'Show shell commands.',
55
55
  '/version': 'Print the wiki-manager version.',
56
56
  '/exit': 'Exit the shell.',
57
- '/workspaces': 'List configured workspaces.',
58
- '/new': 'Create or configure a new workspace.',
57
+ '/workspace': 'List, create, or delete workspaces.',
58
+ '/new': 'Open the setup wizard in the interactive TUI.',
59
59
  '/use': 'Load a workspace and its default config.',
60
60
  '/config': 'Inspect or switch .wikirc.yaml profiles.',
61
61
  '/status': 'Show the current workspace and session state.',
@@ -89,7 +89,9 @@ const SUBCOMMAND_COMPLETION_DESCRIPTIONS = {
89
89
  '/queue': 'Inspect or cancel queued MCP jobs.',
90
90
  '/queue:cancel': 'Cancel a queued or running queue item.',
91
91
  '/queue:clear': 'Clear finished queue items.',
92
- '/workspace:init': 'Legacy form of /new.',
92
+ '/workspace:init': 'Low-level workspace creation.',
93
+ '/workspace:list': 'List configured workspaces.',
94
+ '/workspace:delete': 'Delete one workspace after confirmation.',
93
95
  '/wiki:run': 'Use the low-level llm-wiki CLI fallback.',
94
96
  '/skills:edit': 'Edit one workspace skill file.',
95
97
  '/skills:list': 'List workspace skills.',
@@ -106,7 +108,7 @@ export function createSession() {
106
108
  wikircConfig: null,
107
109
  language: null,
108
110
  mcp: null,
109
- commands: ['help', 'version', 'exit', 'workspaces', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'upload', 'uploads', 'clear', 'chat', 'agent', 'openui', 'queue'],
111
+ commands: ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'upload', 'uploads', 'clear', 'chat', 'agent', 'openui', 'queue'],
110
112
  chatMode: true,
111
113
  llm: null,
112
114
  activities: {},
@@ -216,11 +218,16 @@ function completionValuesFor(parts, inputBuffer, session) {
216
218
  .filter((item) => ['waiting', 'starting', 'running'].includes(item.status))
217
219
  .map((item) => item.id);
218
220
  }
219
- if (command === '/workspace' && tokenIndex === 1) return ['init'];
221
+ if (command === '/workspace') {
222
+ if (tokenIndex === 1) return ['delete', 'init', 'list'];
223
+ if (previousToken === 'delete') return workspaceNames();
224
+ if (parts[1] === 'delete' && tokenIndex === 3) return ['--confirm'];
225
+ }
220
226
  if (command === '/wiki' && tokenIndex === 1) return ['run'];
221
227
  if (command === '/skills' && tokenIndex === 1) return ['edit', 'list', 'run', 'show'];
222
228
  if (command === '/skills' && ['edit', 'run', 'show'].includes(previousToken ?? '')) return skillNames(session);
223
- if ((command === '/start' || command === '/stop' || command === '/logs') && tokenIndex === 1) return serviceNames();
229
+ if ((command === '/start' || command === '/stop') && tokenIndex === 1) return ['agents', ...serviceNames()];
230
+ if (command === '/logs' && tokenIndex === 1) return serviceNames();
224
231
  return [];
225
232
  }
226
233
 
@@ -289,6 +296,11 @@ export function completionDescription(value, parts) {
289
296
  if (command === '/start') return serviceDescription(value) ?? 'Start this Docker Compose service.';
290
297
  if (command === '/stop') return serviceDescription(value) ?? 'Stop this Docker Compose service.';
291
298
  if (command === '/logs') return serviceDescription(value) ?? 'Show logs for this Docker Compose service.';
299
+ if (command === '/workspace') {
300
+ if (parts[1] === 'delete' && value === '--confirm') return 'Confirm workspace deletion.';
301
+ if (parts.at(-1) === 'delete') return 'Delete this workspace.';
302
+ return 'Choose a workspace action.';
303
+ }
292
304
  if (command === '/mcp') return parts[1] === 'call' ? 'Use this MCP server.' : 'Filter tools to this MCP server.';
293
305
  if (command === '/skills') {
294
306
  if (parts.at(-1) === 'edit') return 'Edit this skill.';
@@ -1174,7 +1186,7 @@ async function runTuiShell({ agent, packageJson, session }) {
1174
1186
  rerender();
1175
1187
 
1176
1188
  try {
1177
- const { output: wsOutput } = await handleSlashCommand('/workspaces', { packageJson, session });
1189
+ const { output: wsOutput } = await handleSlashCommand('/workspace list', { packageJson, session });
1178
1190
  if (wsOutput) messages.push({ role: 'command', content: wsOutput });
1179
1191
  } finally {
1180
1192
  clearInterval(spinnerInterval);
@@ -1316,7 +1328,7 @@ async function runTuiShell({ agent, packageJson, session }) {
1316
1328
  }
1317
1329
  return;
1318
1330
  }
1319
- if (key?.ctrl && key.name === 'c') {
1331
+ if ((key?.ctrl || key?.meta) && key.name === 'c') {
1320
1332
  if (busy) {
1321
1333
  currentAbortController?.abort();
1322
1334
  spinnerLabel = 'Interrupting…';
package/src/shell/tui.tsx CHANGED
@@ -6,6 +6,7 @@ import { FileEditorDialog } from './FileEditorDialog';
6
6
  import { LeftPane } from './LeftPane';
7
7
  import { RightPane } from './RightPane';
8
8
  import { SlashDialog } from './SlashDialog';
9
+ import { SetupWizard } from './SetupWizard';
9
10
  import { useSession } from './useSession';
10
11
 
11
12
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -37,7 +38,10 @@ function copyToClipboard(text: string, renderer: unknown) {
37
38
  }
38
39
  }
39
40
 
40
- function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
41
+ function App(props: {
42
+ agent: unknown;
43
+ packageJson: Record<string, unknown>;
44
+ }) {
41
45
  const renderer = useRenderer();
42
46
  const dimensions = useTerminalDimensions();
43
47
  const [spinnerIndex, setSpinnerIndex] = createSignal(0);
@@ -100,11 +104,12 @@ function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
100
104
  });
101
105
 
102
106
  useKeyboard((key) => {
107
+ const keyName = String(key.name ?? '').toLowerCase();
103
108
  if (state.activeEditor()) {
104
- if (key.name === 'escape') state.closeEditor();
109
+ if (keyName === 'escape') state.closeEditor();
105
110
  return;
106
111
  }
107
- if (key.ctrl && key.name === 'c') {
112
+ if ((key.ctrl || key.meta) && keyName === 'c') {
108
113
  if (state.busy()) {
109
114
  state.abort();
110
115
  return;
@@ -121,19 +126,19 @@ function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
121
126
  }, 1600);
122
127
  return;
123
128
  }
124
- if (key.ctrl && key.name === 'q') {
129
+ if (key.ctrl && keyName === 'q') {
125
130
  state.toggleRightTab();
126
131
  return;
127
132
  }
128
133
  if (state.busy()) return;
129
- if (key.name === 'tab') state.completeSelected();
130
- if (key.name === 'pageup') state.scrollConversation(conversationRows());
131
- else if (key.name === 'pagedown') state.scrollConversation(-conversationRows());
132
- if (key.name === 'up' && state.slash()) state.moveCompletion(-1);
133
- else if (key.name === 'down' && state.slash()) state.moveCompletion(1);
134
- else if (key.name === 'up' && !state.input().includes('\n')) state.historyUp();
135
- else if (key.name === 'down' && !state.input().includes('\n')) state.historyDown();
136
- else if (key.name === 'escape') {
134
+ if (keyName === 'tab') state.completeSelected();
135
+ if (keyName === 'pageup') state.scrollConversation(conversationRows());
136
+ else if (keyName === 'pagedown') state.scrollConversation(-conversationRows());
137
+ if (keyName === 'up' && state.slash()) state.moveCompletion(-1);
138
+ else if (keyName === 'down' && state.slash()) state.moveCompletion(1);
139
+ else if (keyName === 'up' && !state.input().includes('\n')) state.historyUp();
140
+ else if (keyName === 'down' && !state.input().includes('\n')) state.historyDown();
141
+ else if (keyName === 'escape') {
137
142
  if (state.slash()) state.dismissSlash();
138
143
  else state.setInput('');
139
144
  }
@@ -196,10 +201,77 @@ function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
196
201
  );
197
202
  }
198
203
 
199
- export async function runOpenTuiShell({ agent, packageJson }: { agent: unknown; packageJson: Record<string, unknown> }) {
204
+ export async function runOpenTuiShell({
205
+ agent,
206
+ packageJson,
207
+ }: {
208
+ agent: unknown;
209
+ packageJson: Record<string, unknown>;
210
+ }) {
200
211
  await render(() => <App agent={agent} packageJson={packageJson} />, {
201
212
  exitOnCtrlC: false,
202
213
  useMouse: true,
203
214
  targetFps: 30,
204
215
  });
216
+ return {};
217
+ }
218
+
219
+ function WizardApp(props: {
220
+ mode: 'startup' | 'setup';
221
+ gaps?: any[];
222
+ initialWorkspaceName?: string;
223
+ initialWorkspacePath?: string | null;
224
+ onDone: () => void;
225
+ }) {
226
+ const renderer = useRenderer();
227
+ const dimensions = useTerminalDimensions();
228
+ const close = () => {
229
+ props.onDone();
230
+ renderer.destroy();
231
+ };
232
+ return (
233
+ <box width="100%" height="100%" backgroundColor="#0B0D12">
234
+ <SetupWizard
235
+ mode={props.mode}
236
+ session={{}}
237
+ gaps={props.gaps}
238
+ width={dimensions().width}
239
+ height={dimensions().height}
240
+ initialRoute={props.mode === 'setup' ? 'workspace-name' : undefined}
241
+ initialWorkspaceName={props.initialWorkspaceName}
242
+ initialWorkspacePath={props.initialWorkspacePath ?? null}
243
+ closeOnDone={props.mode === 'setup'}
244
+ onComplete={close}
245
+ onClose={close}
246
+ />
247
+ </box>
248
+ );
249
+ }
250
+
251
+ export async function runStartupWizard(gaps: any[]) {
252
+ if (!gaps.length) return;
253
+ await new Promise<void>((resolve, reject) => {
254
+ render(() => <WizardApp mode="startup" gaps={gaps} onDone={resolve} />, {
255
+ exitOnCtrlC: false,
256
+ useMouse: true,
257
+ targetFps: 30,
258
+ }).catch(reject);
259
+ });
260
+ }
261
+
262
+ export async function runSetupWizard(options: { workspaceName?: string; workspacePath?: string | null } = {}) {
263
+ await new Promise<void>((resolve, reject) => {
264
+ render(() => (
265
+ <WizardApp
266
+ mode="setup"
267
+ initialWorkspaceName={options.workspaceName}
268
+ initialWorkspacePath={options.workspacePath ?? null}
269
+ onDone={resolve}
270
+ />
271
+ ), {
272
+ exitOnCtrlC: false,
273
+ useMouse: true,
274
+ targetFps: 30,
275
+ }).catch(reject);
276
+ });
205
277
  }
@@ -86,14 +86,22 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
86
86
  session.llm ? 'llm ready' : 'llm limited',
87
87
  ].join(' ');
88
88
  });
89
- const slash = createMemo(() => {
89
+ const matchContext = createMemo(() => {
90
90
  if (input() === dismissedSlashInput()) return null;
91
- const context = completionContext(input(), session);
91
+ return completionContext(input(), session);
92
+ });
93
+ const slash = createMemo(() => {
94
+ const context = matchContext();
92
95
  if (!context) return null;
96
+ const selected = Math.min(selectedCompletion(), Math.max(0, context.matches.length - 1));
97
+ const visibleCount = 10;
98
+ const start = Math.max(0, Math.min(selected - Math.floor(visibleCount / 2), Math.max(0, context.matches.length - visibleCount)));
99
+ const visibleMatches = context.matches.slice(start, start + visibleCount);
93
100
  return {
94
101
  ...context,
95
- selected: Math.min(selectedCompletion(), Math.max(0, context.matches.length - 1)),
96
- items: context.matches.slice(0, 10).map((value: string) => ({
102
+ selected,
103
+ visibleSelected: selected - start,
104
+ items: visibleMatches.map((value: string) => ({
97
105
  value,
98
106
  description: completionDescription(value, context.parts),
99
107
  })),
@@ -249,8 +257,8 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
249
257
 
250
258
  function completeSelected() {
251
259
  const context = slash();
252
- if (!context || context.items.length === 0) return;
253
- const selected = context.items[context.selected]?.value;
260
+ if (!context || context.matches.length === 0) return;
261
+ const selected = context.matches[context.selected];
254
262
  if (!selected) return;
255
263
  const lastSpace = input().lastIndexOf(' ');
256
264
  const base = input().endsWith(' ') ? input() : input().slice(0, lastSpace + 1);
@@ -259,7 +267,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
259
267
  }
260
268
 
261
269
  function moveCompletion(delta: number) {
262
- const count = slash()?.items.length ?? 0;
270
+ const count = slash()?.matches.length ?? 0;
263
271
  if (!count) return;
264
272
  setSelectedCompletion((value) => (value + delta + count) % count);
265
273
  }
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[@]+"${compose_env_args[@]}"} -f "$agents_compose_file" ${cacert_args[@]+"${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[@]+"${compose_env_args[@]}"} -f "$ROOT_DIR/docker-compose.yml" ${cacert_args[@]+"${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[@]+"${compose_env_args[@]}"} -f "$ROOT_DIR/docker-compose.yml" ${cacert_args[@]+"${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"