arkaos 2.17.5 → 2.19.0

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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.17.5
1
+ 2.19.0
@@ -43,7 +43,13 @@ if ([string]::IsNullOrWhiteSpace($newCwd)) { exit 0 }
43
43
  if (-not (Test-Path -LiteralPath $newCwd -PathType Container)) { exit 0 }
44
44
 
45
45
  # ─── Detect ecosystem from ecosystems.json ─────────────────────────────
46
- $ecosystemsFile = Join-Path $env:USERPROFILE '.claude\skills\arka\knowledge\ecosystems.json'
46
+ # Canonical path is %USERPROFILE%\.arkaos\ecosystems.json (ADR 2026-04-17).
47
+ # Falls back to the legacy skill-dir path until v2.21.0.
48
+ $ecosystemsFile = Join-Path $env:USERPROFILE '.arkaos\ecosystems.json'
49
+ if (-not (Test-Path -LiteralPath $ecosystemsFile)) {
50
+ $legacyEcosystems = Join-Path $env:USERPROFILE '.claude\skills\arka\knowledge\ecosystems.json'
51
+ if (Test-Path -LiteralPath $legacyEcosystems) { $ecosystemsFile = $legacyEcosystems }
52
+ }
47
53
  $ecosystem = ''
48
54
  $ecosystemName = ''
49
55
 
@@ -115,15 +121,17 @@ if (Test-Path -LiteralPath $composerJson) {
115
121
 
116
122
  # ─── Check for project descriptor ─────────────────────────────────────
117
123
  $dirName = Split-Path -Leaf $newCwd.TrimEnd('\','/')
118
- $projectsDir = Join-Path $env:USERPROFILE '.claude\skills\arka\projects'
119
- $descriptorFile = Join-Path $projectsDir "$dirName.md"
120
- $descriptorDir = Join-Path (Join-Path $projectsDir $dirName) 'PROJECT.md'
124
+ $newProjectsDir = Join-Path $env:USERPROFILE '.arkaos\projects'
125
+ $legacyProjectsDir = Join-Path $env:USERPROFILE '.claude\skills\arka\projects'
121
126
 
122
127
  $descriptor = ''
123
- if (Test-Path -LiteralPath $descriptorFile) {
124
- $descriptor = $descriptorFile
125
- } elseif (Test-Path -LiteralPath $descriptorDir) {
126
- $descriptor = $descriptorDir
128
+ foreach ($candidate in @(
129
+ (Join-Path $newProjectsDir "$dirName.md"),
130
+ (Join-Path (Join-Path $newProjectsDir $dirName) 'PROJECT.md'),
131
+ (Join-Path $legacyProjectsDir "$dirName.md"),
132
+ (Join-Path (Join-Path $legacyProjectsDir $dirName) 'PROJECT.md')
133
+ )) {
134
+ if (Test-Path -LiteralPath $candidate) { $descriptor = $candidate; break }
127
135
  }
128
136
 
129
137
  # ─── Build context output ─────────────────────────────────────────────
@@ -13,7 +13,13 @@ if [ -z "$NEW_CWD" ] || [ ! -d "$NEW_CWD" ]; then
13
13
  fi
14
14
 
15
15
  # ─── Detect ecosystem from ecosystems.json ─────────────────────────────
16
- ECOSYSTEMS_FILE="$HOME/.claude/skills/arka/knowledge/ecosystems.json"
16
+ # Canonical path is ~/.arkaos/ecosystems.json (ADR 2026-04-17). During the
17
+ # deprecation window we fall back to the legacy skill-dir path. The legacy
18
+ # fallback is removed in v2.21.0.
19
+ ECOSYSTEMS_FILE="$HOME/.arkaos/ecosystems.json"
20
+ if [ ! -f "$ECOSYSTEMS_FILE" ] && [ -f "$HOME/.claude/skills/arka/knowledge/ecosystems.json" ]; then
21
+ ECOSYSTEMS_FILE="$HOME/.claude/skills/arka/knowledge/ecosystems.json"
22
+ fi
17
23
  ECOSYSTEM=""
18
24
  ECOSYSTEM_NAME=""
19
25
 
@@ -75,16 +81,19 @@ elif [ -f "$NEW_CWD/pyproject.toml" ]; then
75
81
  fi
76
82
 
77
83
  # ─── Check for project descriptor ─────────────────────────────────────
84
+ # New canonical path: ~/.arkaos/projects/. Legacy fallback remains until v2.21.0.
78
85
  DIR_NAME=$(basename "$NEW_CWD")
79
86
  DESCRIPTOR=""
80
- DESCRIPTOR_FILE="$HOME/.claude/skills/arka/projects/${DIR_NAME}.md"
81
- DESCRIPTOR_DIR="$HOME/.claude/skills/arka/projects/${DIR_NAME}/PROJECT.md"
82
-
83
- if [ -f "$DESCRIPTOR_FILE" ]; then
84
- DESCRIPTOR="$DESCRIPTOR_FILE"
85
- elif [ -f "$DESCRIPTOR_DIR" ]; then
86
- DESCRIPTOR="$DESCRIPTOR_DIR"
87
- fi
87
+ for CANDIDATE in \
88
+ "$HOME/.arkaos/projects/${DIR_NAME}.md" \
89
+ "$HOME/.arkaos/projects/${DIR_NAME}/PROJECT.md" \
90
+ "$HOME/.claude/skills/arka/projects/${DIR_NAME}.md" \
91
+ "$HOME/.claude/skills/arka/projects/${DIR_NAME}/PROJECT.md"; do
92
+ if [ -f "$CANDIDATE" ]; then
93
+ DESCRIPTOR="$CANDIDATE"
94
+ break
95
+ fi
96
+ done
88
97
 
89
98
  # ─── Build context output ─────────────────────────────────────────────
90
99
  CONTEXT=""
@@ -1,36 +1,249 @@
1
1
  # ArkaOS MCP Policy Registry
2
2
  # Controls which MCPs load eagerly vs deferred per project stack/ecosystem.
3
- # Rules evaluated top-to-bottom; first match wins. "ambiguous: ['*']" means
4
- # all other MCPs defer to AI (or fallback heuristic when AI unavailable).
3
+ # Rules evaluated top-to-bottom; first match wins.
4
+ #
5
+ # v2: fully deterministic — every known MCP is explicitly active or deferred
6
+ # per stack. No ambiguous fallthrough required. AI decider remains as an
7
+ # optional extension for future user-added MCPs.
5
8
 
6
- version: 1
9
+ version: 2
7
10
  policies:
11
+ # --- Backend: Laravel / PHP ---
8
12
  - match:
9
13
  stack_includes: [laravel, php]
10
- active: [context7, gh-grep, postgres, supabase]
11
- deferred: [canva, clickup, firecrawl, chrome, gmail, calendar, claude-in-chrome]
14
+ active:
15
+ - arka-prompts
16
+ - obsidian
17
+ - context7
18
+ - gh-grep
19
+ - memory-bank
20
+ - postgres
21
+ - supabase
22
+ - laravel-boost
23
+ - serena
24
+ - sentry
25
+ deferred:
26
+ - playwright
27
+ - nuxt
28
+ - nuxt-ui
29
+ - next-devtools
30
+ - canva
31
+ - clickup
32
+ - firecrawl
33
+ - slack
34
+ - discord
35
+ - whatsapp
36
+ - teams
37
+ - shopify-dev
38
+ - mirakl
12
39
  ambiguous: []
13
40
 
41
+ # --- Frontend: Nuxt / Vue ---
14
42
  - match:
15
- stack_includes: [nuxt, vue, react, next]
16
- active: [context7, gh-grep, playwright, claude-in-chrome]
17
- deferred: [postgres, supabase, canva, clickup, gmail, calendar]
43
+ stack_includes: [nuxt, vue]
44
+ active:
45
+ - arka-prompts
46
+ - obsidian
47
+ - context7
48
+ - gh-grep
49
+ - memory-bank
50
+ - playwright
51
+ - nuxt
52
+ - nuxt-ui
53
+ - sentry
54
+ deferred:
55
+ - postgres
56
+ - supabase
57
+ - laravel-boost
58
+ - serena
59
+ - next-devtools
60
+ - canva
61
+ - clickup
62
+ - firecrawl
63
+ - slack
64
+ - discord
65
+ - whatsapp
66
+ - teams
67
+ - shopify-dev
68
+ - mirakl
18
69
  ambiguous: []
19
70
 
71
+ # --- Frontend: React / Next ---
72
+ - match:
73
+ stack_includes: [next, react]
74
+ active:
75
+ - arka-prompts
76
+ - obsidian
77
+ - context7
78
+ - gh-grep
79
+ - memory-bank
80
+ - playwright
81
+ - next-devtools
82
+ - sentry
83
+ deferred:
84
+ - postgres
85
+ - supabase
86
+ - laravel-boost
87
+ - serena
88
+ - nuxt
89
+ - nuxt-ui
90
+ - canva
91
+ - clickup
92
+ - firecrawl
93
+ - slack
94
+ - discord
95
+ - whatsapp
96
+ - teams
97
+ - shopify-dev
98
+ - mirakl
99
+ ambiguous: []
100
+
101
+ # --- E-commerce (Shopify / Mirakl) ---
102
+ - match:
103
+ stack_includes: [shopify]
104
+ active:
105
+ - arka-prompts
106
+ - obsidian
107
+ - context7
108
+ - gh-grep
109
+ - memory-bank
110
+ - playwright
111
+ - shopify-dev
112
+ - mirakl
113
+ - sentry
114
+ deferred:
115
+ - postgres
116
+ - supabase
117
+ - laravel-boost
118
+ - serena
119
+ - nuxt
120
+ - nuxt-ui
121
+ - next-devtools
122
+ - canva
123
+ - clickup
124
+ - firecrawl
125
+ - slack
126
+ - discord
127
+ - whatsapp
128
+ - teams
129
+ ambiguous: []
130
+
131
+ # --- Python projects ---
132
+ - match:
133
+ stack_includes: [python]
134
+ active:
135
+ - arka-prompts
136
+ - obsidian
137
+ - context7
138
+ - gh-grep
139
+ - memory-bank
140
+ - sentry
141
+ deferred:
142
+ - playwright
143
+ - postgres
144
+ - supabase
145
+ - laravel-boost
146
+ - serena
147
+ - nuxt
148
+ - nuxt-ui
149
+ - next-devtools
150
+ - canva
151
+ - clickup
152
+ - firecrawl
153
+ - slack
154
+ - discord
155
+ - whatsapp
156
+ - teams
157
+ - shopify-dev
158
+ - mirakl
159
+ ambiguous: []
160
+
161
+ # --- Ecosystem: marketing ---
20
162
  - match:
21
163
  ecosystem: marketing
22
- active: [canva, gmail, calendar, firecrawl, clickup]
23
- deferred: [postgres, supabase, playwright]
164
+ active:
165
+ - arka-prompts
166
+ - obsidian
167
+ - context7
168
+ - memory-bank
169
+ - canva
170
+ - firecrawl
171
+ - clickup
172
+ deferred:
173
+ - gh-grep
174
+ - postgres
175
+ - supabase
176
+ - playwright
177
+ - laravel-boost
178
+ - serena
179
+ - nuxt
180
+ - nuxt-ui
181
+ - next-devtools
182
+ - sentry
183
+ - slack
184
+ - discord
185
+ - whatsapp
186
+ - teams
187
+ - shopify-dev
188
+ - mirakl
24
189
  ambiguous: []
25
190
 
191
+ # --- Ecosystem: content ---
26
192
  - match:
27
193
  ecosystem: content
28
- active: [canva, firecrawl, youtube-transcript]
29
- deferred: [postgres, clickup]
194
+ active:
195
+ - arka-prompts
196
+ - obsidian
197
+ - context7
198
+ - memory-bank
199
+ - canva
200
+ - firecrawl
201
+ deferred:
202
+ - gh-grep
203
+ - postgres
204
+ - supabase
205
+ - playwright
206
+ - laravel-boost
207
+ - serena
208
+ - nuxt
209
+ - nuxt-ui
210
+ - next-devtools
211
+ - sentry
212
+ - clickup
213
+ - slack
214
+ - discord
215
+ - whatsapp
216
+ - teams
217
+ - shopify-dev
218
+ - mirakl
30
219
  ambiguous: []
31
220
 
221
+ # --- Default: minimal safe set ---
32
222
  - match:
33
223
  default: true
34
- active: [context7]
35
- deferred: []
36
- ambiguous: ["*"]
224
+ active:
225
+ - arka-prompts
226
+ - obsidian
227
+ - context7
228
+ - gh-grep
229
+ - memory-bank
230
+ deferred:
231
+ - postgres
232
+ - supabase
233
+ - playwright
234
+ - laravel-boost
235
+ - serena
236
+ - nuxt
237
+ - nuxt-ui
238
+ - next-devtools
239
+ - sentry
240
+ - canva
241
+ - clickup
242
+ - firecrawl
243
+ - slack
244
+ - discord
245
+ - whatsapp
246
+ - teams
247
+ - shopify-dev
248
+ - mirakl
249
+ ambiguous: []
@@ -0,0 +1,115 @@
1
+ """Canonical resolver for ArkaOS user-local data paths.
2
+
3
+ User-mutable data lives under ~/.arkaos/. The installed skill bundle at
4
+ ~/.claude/skills/arka/ is read-only and installer-managed. Two historical
5
+ paths still receive user writes in some installs:
6
+
7
+ ~/.claude/skills/arka/projects/*.md
8
+ ~/.claude/skills/arka/knowledge/ecosystems.json
9
+
10
+ This module returns the canonical new path when present, falls back to the
11
+ legacy path with a one-shot deprecation warning, and returns None when
12
+ neither exists. Callers treat None as empty. See ADR
13
+ docs/adr/2026-04-17-user-data-separation.md.
14
+
15
+ Legacy fallback sunsets in v2.21.0.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from pathlib import Path
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _LEGACY_SKILLS_ROOT = Path.home() / ".claude" / "skills" / "arka"
26
+ _USER_DATA_ROOT = Path.home() / ".arkaos"
27
+
28
+ _warned: set[str] = set()
29
+
30
+
31
+ def user_data_root() -> Path:
32
+ """Root directory for ArkaOS user-local state."""
33
+ return _USER_DATA_ROOT
34
+
35
+
36
+ def projects_dir() -> Path | None:
37
+ """Resolve the user projects directory.
38
+
39
+ Returns the new path when it exists, else the legacy path with a
40
+ one-shot deprecation warning, else None.
41
+ """
42
+ new = _USER_DATA_ROOT / "projects"
43
+ legacy = _LEGACY_SKILLS_ROOT / "projects"
44
+ return _resolve_with_fallback("projects_dir", new, legacy)
45
+
46
+
47
+ def ecosystems_file() -> Path | None:
48
+ """Resolve the ecosystems registry file.
49
+
50
+ Returns the new path when it exists, else the legacy path with a
51
+ one-shot deprecation warning, else None.
52
+ """
53
+ new = _USER_DATA_ROOT / "ecosystems.json"
54
+ legacy = _LEGACY_SKILLS_ROOT / "knowledge" / "ecosystems.json"
55
+ return _resolve_with_fallback("ecosystems_file", new, legacy)
56
+
57
+
58
+ def projects_dir_for_write() -> Path:
59
+ """Return the write target for user project descriptors.
60
+
61
+ Writers always target the new path. The directory is created if absent.
62
+ """
63
+ new = _USER_DATA_ROOT / "projects"
64
+ new.mkdir(parents=True, exist_ok=True)
65
+ return new
66
+
67
+
68
+ def ecosystems_file_for_write() -> Path:
69
+ """Return the write target for the ecosystems registry.
70
+
71
+ Writers always target the new path. Parent directory is created if absent.
72
+ """
73
+ new = _USER_DATA_ROOT / "ecosystems.json"
74
+ new.parent.mkdir(parents=True, exist_ok=True)
75
+ return new
76
+
77
+
78
+ def legacy_projects_dir() -> Path:
79
+ """Legacy projects directory (for migration tooling only)."""
80
+ return _LEGACY_SKILLS_ROOT / "projects"
81
+
82
+
83
+ def legacy_ecosystems_file() -> Path:
84
+ """Legacy ecosystems file (for migration tooling only)."""
85
+ return _LEGACY_SKILLS_ROOT / "knowledge" / "ecosystems.json"
86
+
87
+
88
+ def reset_warnings() -> None:
89
+ """Clear the per-process deprecation warning cache.
90
+
91
+ Intended for tests. Real runtime should emit each warning exactly once.
92
+ """
93
+ _warned.clear()
94
+
95
+
96
+ def _resolve_with_fallback(kind: str, new: Path, legacy: Path) -> Path | None:
97
+ if new.exists():
98
+ return new
99
+ if legacy.exists():
100
+ _warn_once(kind, legacy)
101
+ return legacy
102
+ return None
103
+
104
+
105
+ def _warn_once(kind: str, legacy: Path) -> None:
106
+ if kind in _warned:
107
+ return
108
+ _warned.add(kind)
109
+ logger.warning(
110
+ "ArkaOS: reading %s from legacy location %s. "
111
+ "This path is deprecated and will be removed in v2.21.0. "
112
+ "Run `npx arkaos@latest migrate-user-data` or `/arka update` to move your data to ~/.arkaos/.",
113
+ kind,
114
+ legacy,
115
+ )
@@ -11,6 +11,10 @@ import re
11
11
  import sys
12
12
  from pathlib import Path
13
13
 
14
+ from core.runtime.user_paths import (
15
+ ecosystems_file as resolve_ecosystems_file,
16
+ projects_dir as resolve_projects_dir,
17
+ )
14
18
  from core.sync.manifest import build_manifest
15
19
  from core.sync.discovery import discover_all_projects
16
20
  from core.sync.mcp_optimizer import optimize_all_mcps
@@ -169,13 +173,29 @@ def _parse_scan_dirs(projects_dir_str: str) -> list[Path]:
169
173
 
170
174
 
171
175
  def _discover_projects(arkaos_home: Path, skills_dir: Path) -> list:
172
- """Combine profile.json dirs, descriptor dir, and ecosystems into projects."""
173
- descriptor_dir = skills_dir / "arka" / "projects"
174
- ecosystems_file = skills_dir / "arka" / "knowledge" / "ecosystems.json"
176
+ """Combine profile.json dirs, descriptor dir, and ecosystems into projects.
177
+
178
+ Project descriptors and the ecosystems registry are user-local data and
179
+ live under ~/.arkaos/ (see ADR 2026-04-17-user-data-separation). During
180
+ the deprecation window, reads fall back to the legacy paths under
181
+ skills_dir with a one-shot warning. `skills_dir` is kept in the
182
+ signature for backward compatibility and test ergonomics but is no
183
+ longer consulted for user data.
184
+ """
185
+ del skills_dir # retained for signature stability; unused.
186
+
187
+ descriptor_dir = resolve_projects_dir()
188
+ ecosystems_path = resolve_ecosystems_file()
189
+
190
+ # discover_all_projects treats missing paths as empty; pass a stable
191
+ # sentinel when the resolver returned None so downstream .exists()
192
+ # checks short-circuit cleanly.
193
+ descriptor_dir = descriptor_dir or (Path.home() / ".arkaos" / "projects")
194
+ ecosystems_path = ecosystems_path or (Path.home() / ".arkaos" / "ecosystems.json")
175
195
 
176
196
  scan_dirs = _load_scan_dirs_from_profile(arkaos_home)
177
197
 
178
- return discover_all_projects(descriptor_dir, scan_dirs, ecosystems_file)
198
+ return discover_all_projects(descriptor_dir, scan_dirs, ecosystems_path)
179
199
 
180
200
 
181
201
  def _load_scan_dirs_from_profile(arkaos_home: Path) -> list[Path]:
package/installer/cli.js CHANGED
@@ -41,6 +41,7 @@ Usage:
41
41
  npx arkaos init Initialize project config (.arkaos.json)
42
42
  npx arkaos update Update to latest version
43
43
  npx arkaos migrate Migrate from v1 to v2
44
+ npx arkaos migrate-user-data Move user data (~/.claude/skills/arka/ → ~/.arkaos/)
44
45
  npx arkaos dashboard Start monitoring dashboard
45
46
  npx arkaos keys Manage API keys (OpenAI, fal.ai, etc.)
46
47
  npx arkaos doctor Run health checks
@@ -102,6 +103,12 @@ async function main() {
102
103
  await migrate();
103
104
  break;
104
105
 
106
+ case "migrate-user-data": {
107
+ const { migrateUserData, printMigrationReport } = await import("./migrate-user-data.js");
108
+ printMigrationReport(migrateUserData());
109
+ break;
110
+ }
111
+
105
112
  case "keys": {
106
113
  const { keys: keysCmd } = await import("./keys.js");
107
114
  await keysCmd(positionals.slice(1));
@@ -34,8 +34,28 @@ export async function install({ runtime, path, force }) {
34
34
  // ═══ Step 1: Create directories ═══
35
35
  step(1, 14, "Creating directories...");
36
36
  ensureDir(installDir);
37
- const dirs = ["config", "config/hooks", "agents", "media", "session-digests", "vault"];
37
+ const dirs = ["config", "config/hooks", "agents", "media", "session-digests", "vault", "projects", "logs"];
38
38
  for (const d of dirs) ensureDir(join(installDir, d));
39
+
40
+ // Seed an empty ecosystems.json the first time; never overwrite existing user data.
41
+ const ecosystemsPath = join(installDir, "ecosystems.json");
42
+ if (!existsSync(ecosystemsPath)) {
43
+ writeFileSync(
44
+ ecosystemsPath,
45
+ JSON.stringify(
46
+ {
47
+ _meta: {
48
+ description: "ArkaOS — user-local ecosystem registry",
49
+ note: "Populated by /arka onboard. Never committed to the public repo.",
50
+ updated: new Date().toISOString().slice(0, 10),
51
+ },
52
+ ecosystems: {},
53
+ },
54
+ null,
55
+ 2,
56
+ ) + "\n",
57
+ );
58
+ }
39
59
  ok(`${dirs.length + 1} directories ready`);
40
60
 
41
61
  // ═══ Step 2: Detect v1 installation ═══
@@ -0,0 +1,110 @@
1
+ import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ const LEGACY_SKILLS_ROOT = join(homedir(), ".claude", "skills", "arka");
6
+ const USER_DATA_ROOT = join(homedir(), ".arkaos");
7
+
8
+ const LEGACY_PROJECTS = join(LEGACY_SKILLS_ROOT, "projects");
9
+ const LEGACY_ECOSYSTEMS = join(LEGACY_SKILLS_ROOT, "knowledge", "ecosystems.json");
10
+ const NEW_PROJECTS = join(USER_DATA_ROOT, "projects");
11
+ const NEW_ECOSYSTEMS = join(USER_DATA_ROOT, "ecosystems.json");
12
+ const LOGS_DIR = join(USER_DATA_ROOT, "logs");
13
+
14
+ /**
15
+ * Move user-local project descriptors and ecosystems registry from the
16
+ * legacy skill directory to ~/.arkaos/. Idempotent: items already present
17
+ * at the destination are left alone and logged as conflicts.
18
+ *
19
+ * @returns {{ moved: string[], skipped: string[], conflicts: string[], logPath: string|null }}
20
+ */
21
+ export function migrateUserData({ dryRun = false } = {}) {
22
+ const moved = [];
23
+ const skipped = [];
24
+ const conflicts = [];
25
+
26
+ if (!existsSync(USER_DATA_ROOT)) {
27
+ mkdirSync(USER_DATA_ROOT, { recursive: true });
28
+ }
29
+ if (!existsSync(NEW_PROJECTS)) {
30
+ mkdirSync(NEW_PROJECTS, { recursive: true });
31
+ }
32
+
33
+ if (existsSync(LEGACY_PROJECTS) && statSync(LEGACY_PROJECTS).isDirectory()) {
34
+ for (const entry of readdirSync(LEGACY_PROJECTS)) {
35
+ const src = join(LEGACY_PROJECTS, entry);
36
+ const dst = join(NEW_PROJECTS, entry);
37
+ if (existsSync(dst)) {
38
+ conflicts.push(`projects/${entry}: destination already present, left source untouched`);
39
+ continue;
40
+ }
41
+ if (dryRun) {
42
+ moved.push(`projects/${entry} (dry-run)`);
43
+ } else {
44
+ try {
45
+ renameSync(src, dst);
46
+ moved.push(`projects/${entry}`);
47
+ } catch (err) {
48
+ conflicts.push(`projects/${entry}: ${err.message}`);
49
+ }
50
+ }
51
+ }
52
+ } else {
53
+ skipped.push("projects/: legacy directory absent");
54
+ }
55
+
56
+ if (existsSync(LEGACY_ECOSYSTEMS)) {
57
+ if (existsSync(NEW_ECOSYSTEMS)) {
58
+ conflicts.push("ecosystems.json: destination already present, left source untouched");
59
+ } else if (dryRun) {
60
+ moved.push("ecosystems.json (dry-run)");
61
+ } else {
62
+ try {
63
+ renameSync(LEGACY_ECOSYSTEMS, NEW_ECOSYSTEMS);
64
+ moved.push("ecosystems.json");
65
+ } catch (err) {
66
+ conflicts.push(`ecosystems.json: ${err.message}`);
67
+ }
68
+ }
69
+ } else {
70
+ skipped.push("ecosystems.json: legacy file absent");
71
+ }
72
+
73
+ let logPath = null;
74
+ if (moved.length > 0 || conflicts.length > 0) {
75
+ if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
76
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
77
+ logPath = join(LOGS_DIR, `migration-${stamp}.log`);
78
+ const body = [
79
+ `ArkaOS user-data migration — ${new Date().toISOString()}`,
80
+ `Source: ${LEGACY_SKILLS_ROOT}`,
81
+ `Destination: ${USER_DATA_ROOT}`,
82
+ `Dry-run: ${dryRun}`,
83
+ "",
84
+ `Moved (${moved.length}):`,
85
+ ...moved.map(m => ` - ${m}`),
86
+ "",
87
+ `Conflicts (${conflicts.length}):`,
88
+ ...conflicts.map(c => ` - ${c}`),
89
+ "",
90
+ `Skipped (${skipped.length}):`,
91
+ ...skipped.map(s => ` - ${s}`),
92
+ "",
93
+ ].join("\n");
94
+ if (!dryRun) writeFileSync(logPath, body);
95
+ }
96
+
97
+ return { moved, skipped, conflicts, logPath };
98
+ }
99
+
100
+ export function printMigrationReport(result) {
101
+ const { moved, conflicts, logPath } = result;
102
+ if (moved.length === 0 && conflicts.length === 0) {
103
+ console.log(" ✓ User data already migrated, nothing to move.");
104
+ return;
105
+ }
106
+ console.log(` ✓ Migration: ${moved.length} moved, ${conflicts.length} conflicts`);
107
+ for (const m of moved) console.log(` + ${m}`);
108
+ for (const c of conflicts) console.log(` ! ${c}`);
109
+ if (logPath) console.log(` Log: ${logPath}`);
110
+ }