arkaos 2.22.1 → 2.28.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/README.md +105 -9
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -0
- package/arka/skills/bootstrap-agent/SKILL.md +76 -0
- package/arka/skills/comfyui/SKILL.md +2 -2
- package/arka/skills/comfyui/references/workflows.md +6 -6
- package/arka/skills/costs/SKILL.md +11 -0
- package/arka/skills/design-ops/SKILL.md +68 -0
- package/arka/skills/design-ops/scripts/extract-colors.py +86 -0
- package/arka/skills/design-ops/scripts/shadcn-tokens.py +59 -0
- package/arka/skills/design-ops/scripts/wcag-contrast.py +92 -0
- package/arka/skills/research/SKILL.md +117 -0
- package/config/cognition/prompts/dreaming.md +3 -3
- package/config/cognition/prompts/research.md +4 -4
- package/config/constitution.yaml +4 -0
- package/config/hooks/post-tool-use.sh +17 -0
- package/config/hooks/user-prompt-submit.sh +22 -1
- package/core/agents/__pycache__/loader.cpython-313.pyc +0 -0
- package/core/agents/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/agents/loader.py +4 -1
- package/core/agents/schema.py +29 -0
- package/core/cognition/__pycache__/auto_documentor.cpython-313.pyc +0 -0
- package/core/cognition/__pycache__/retrieval.cpython-313.pyc +0 -0
- package/core/cognition/capture/__pycache__/collector.cpython-313.pyc +0 -0
- package/core/cognition/capture/collector.py +10 -3
- package/core/cognition/retrieval.py +383 -0
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +5 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/writer.cpython-313.pyc +0 -0
- package/core/obsidian/writer.py +5 -4
- package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/path_resolver.cpython-313.pyc +0 -0
- package/core/runtime/path_resolver.py +202 -0
- package/core/shared/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/safe_session_id.cpython-313.pyc +0 -0
- package/core/specs/SPEC-installer-cross-os.md +227 -0
- package/core/specs/SPEC-paths-portability.md +209 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/departments/brand/agents/design-ops/design-ops-lead.yaml +78 -0
- package/departments/brand/agents/design-ops/extraction-script-writer.yaml +74 -0
- package/departments/brand/agents/design-ops/shadcn-padronizer.yaml +76 -0
- package/departments/brand/agents/design-ops/wcag-auditor.yaml +76 -0
- package/departments/dev/skills/mcp/SKILL.md +16 -0
- package/departments/dev/skills/scaffold/SKILL.md +4 -4
- package/departments/kb/skills/knowledge/SKILL.md +1 -1
- package/departments/ops/skills/operations/SKILL.md +1 -1
- package/installer/cli.js +2 -1
- package/installer/doctor.js +48 -0
- package/installer/index.js +26 -1
- package/installer/migrate-user-data.js +17 -1
- package/installer/migrations/v3_path_schema.js +109 -0
- package/installer/package-manager.js +191 -0
- package/installer/path-resolver.js +124 -0
- package/installer/system-tools.js +243 -0
- package/installer/update.js +24 -3
- package/knowledge/obsidian-config.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
id: wcag-auditor-oren
|
|
2
|
+
name: Oren
|
|
3
|
+
role: Accessibility Auditor
|
|
4
|
+
department: brand
|
|
5
|
+
tier: 2
|
|
6
|
+
model: sonnet
|
|
7
|
+
|
|
8
|
+
parent_squad: brand
|
|
9
|
+
sub_squad_role: wcag-auditor
|
|
10
|
+
|
|
11
|
+
behavioral_dna:
|
|
12
|
+
disc:
|
|
13
|
+
primary: C
|
|
14
|
+
secondary: S
|
|
15
|
+
communication_style: "Methodical, cites the exact WCAG criterion, never bluffs"
|
|
16
|
+
under_pressure: "Reports findings in priority order with severity; refuses to skip levels"
|
|
17
|
+
motivator: "Interfaces every human can actually use, including with assistive tech"
|
|
18
|
+
enneagram:
|
|
19
|
+
type: 1
|
|
20
|
+
wing: 2
|
|
21
|
+
core_motivation: "Make accessibility the default rather than a retrofit"
|
|
22
|
+
core_fear: "A real user blocked by a contrast ratio or focus-trap nobody caught"
|
|
23
|
+
subtype: social
|
|
24
|
+
big_five:
|
|
25
|
+
openness: 55
|
|
26
|
+
conscientiousness: 92
|
|
27
|
+
extraversion: 38
|
|
28
|
+
agreeableness: 68
|
|
29
|
+
neuroticism: 32
|
|
30
|
+
mbti:
|
|
31
|
+
type: ISTJ
|
|
32
|
+
|
|
33
|
+
mental_models:
|
|
34
|
+
primary:
|
|
35
|
+
- "WCAG 2.2 (AA + AAA)"
|
|
36
|
+
- "POUR Principles (Perceivable, Operable, Understandable, Robust)"
|
|
37
|
+
- "ARIA Authoring Practices"
|
|
38
|
+
secondary:
|
|
39
|
+
- "Inclusive Design Microsoft toolkit"
|
|
40
|
+
- "Cognitive Load Theory (Sweller)"
|
|
41
|
+
- "Universal Design (Mace)"
|
|
42
|
+
|
|
43
|
+
authority:
|
|
44
|
+
orchestrate: false
|
|
45
|
+
approve_quality: false
|
|
46
|
+
delegates_to: []
|
|
47
|
+
escalates_to: design-ops-lead-iris
|
|
48
|
+
|
|
49
|
+
expertise:
|
|
50
|
+
domains:
|
|
51
|
+
- WCAG 2.2 AA / AAA conformance auditing
|
|
52
|
+
- color contrast ratio analysis (4.5:1 text, 3:1 large text and UI)
|
|
53
|
+
- keyboard navigation and focus management
|
|
54
|
+
- screen reader compatibility (NVDA, JAWS, VoiceOver)
|
|
55
|
+
- ARIA pattern review (no-redundant-roles, correct landmarks)
|
|
56
|
+
- cognitive accessibility (reading level, error recovery)
|
|
57
|
+
- accessibility statement drafting
|
|
58
|
+
frameworks:
|
|
59
|
+
- WCAG 2.2
|
|
60
|
+
- WAI-ARIA 1.2
|
|
61
|
+
- ATAG 2.0
|
|
62
|
+
- EAA / EN 301 549
|
|
63
|
+
- axe-core ruleset
|
|
64
|
+
- Section 508
|
|
65
|
+
depth: master
|
|
66
|
+
years_equivalent: 11
|
|
67
|
+
|
|
68
|
+
communication:
|
|
69
|
+
language: en
|
|
70
|
+
tone: "neutral, criterion-cited, never moralising"
|
|
71
|
+
vocabulary_level: advanced
|
|
72
|
+
preferred_format: "issue tables (severity / criterion / location / fix), conformance reports"
|
|
73
|
+
avoid:
|
|
74
|
+
- "claiming AAA without measurement"
|
|
75
|
+
- "blocking a ship on a Level AAA edge case"
|
|
76
|
+
- "fixing accessibility late instead of as a design constraint"
|
|
@@ -118,3 +118,19 @@ The user should set these in their shell profile or project `.env`, or run `bash
|
|
|
118
118
|
- `DISCORD_TOKEN` — Discord bot integration
|
|
119
119
|
- `WHATSAPP_API_TOKEN` / `WHATSAPP_PHONE_ID` — WhatsApp Business API
|
|
120
120
|
- `TEAMS_APP_ID` / `TEAMS_APP_SECRET` — Microsoft Teams
|
|
121
|
+
|
|
122
|
+
## Recommended baseline (Orgo 2026-05-13)
|
|
123
|
+
|
|
124
|
+
Three MCPs that the 2026-05-13 podcast pattern recommends for every
|
|
125
|
+
agent-business setup. They are *optional* in the registry but worth
|
|
126
|
+
adopting deliberately:
|
|
127
|
+
|
|
128
|
+
| MCP | Why | When to add |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| **Composio** | One connector, thousands of apps (Gmail, Slack, Notion, GitHub). Handles auth + tool calling without per-tool wiring. | Any agent that needs broad SaaS integration. Cuts setup time on every new customer. |
|
|
131
|
+
| **Agent Mail** | Gives each agent a real email address that it can read and send from. Adds the "digital employee" feel that closes deals. | Customer-facing agents, executive assistants, follow-up workflows. |
|
|
132
|
+
| **XMCP** | Twitter / X's official MCP. Brings community wisdom into the agent's context — recent threads, hot takes, real-time discourse. | Research-heavy workflows, content / community / sales squads. |
|
|
133
|
+
|
|
134
|
+
These pair naturally with the existing `Firecrawl` (deep scrape),
|
|
135
|
+
`Context7` (official docs), and `Perplexity` (real-time web). The
|
|
136
|
+
`/arka research` skill fans out across all five.
|
|
@@ -16,7 +16,7 @@ Create new projects from real git repositories with full automation: dependencie
|
|
|
16
16
|
|
|
17
17
|
| Command | Git Repository | Stack |
|
|
18
18
|
|---------|---------------|-------|
|
|
19
|
-
| `/dev scaffold laravel <name>` | `
|
|
19
|
+
| `/dev scaffold laravel <name>` | `https://${GIT_HOST}/laravel/laravel.git` (override with `ARKAOS_LARAVEL_STARTER_REPO` env) | Laravel + Herd |
|
|
20
20
|
| `/dev scaffold nuxt-saas <name>` | `https://github.com/nuxt-ui-templates/dashboard.git` | Nuxt 3 Dashboard |
|
|
21
21
|
| `/dev scaffold nuxt-landing <name>` | `https://github.com/nuxt-ui-templates/landing.git` | Nuxt 3 Landing |
|
|
22
22
|
| `/dev scaffold nuxt-docs <name>` | `https://github.com/nuxt-ui-templates/docs.git` | Nuxt 3 Docs |
|
|
@@ -136,7 +136,7 @@ cp PROJECT.md "$ARKA_OS/projects/<name>/PROJECT.md"
|
|
|
136
136
|
|
|
137
137
|
Create pages in the Obsidian vault:
|
|
138
138
|
|
|
139
|
-
**Main page:**
|
|
139
|
+
**Main page:** `${VAULT_PATH}/Projects/<name>/Home.md`
|
|
140
140
|
```markdown
|
|
141
141
|
---
|
|
142
142
|
type: project
|
|
@@ -165,7 +165,7 @@ tags:
|
|
|
165
165
|
- *Part of the [[Projects MOC]]*
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
**Architecture page:**
|
|
168
|
+
**Architecture page:** `${VAULT_PATH}/Projects/<name>/Architecture/decisions.md`
|
|
169
169
|
```markdown
|
|
170
170
|
---
|
|
171
171
|
type: adr-log
|
|
@@ -242,7 +242,7 @@ Both directories get their respective dependencies installed, and the full-stack
|
|
|
242
242
|
|
|
243
243
|
## Error Handling
|
|
244
244
|
|
|
245
|
-
- If `git clone` fails: check SSH keys
|
|
245
|
+
- If `git clone` fails: check SSH keys / repo access permissions for the configured `${GIT_HOST}`
|
|
246
246
|
- If `composer install` fails: check PHP version (`php -v`, need 8.3+)
|
|
247
247
|
- If `pnpm install` fails: check Node version (`node -v`, need 18+)
|
|
248
248
|
- If `herd link` fails: check Herd is installed and running
|
|
@@ -22,7 +22,7 @@ Dynamic knowledge acquisition and management system. Learn from YouTube videos,
|
|
|
22
22
|
|
|
23
23
|
## Obsidian Configuration
|
|
24
24
|
|
|
25
|
-
- **Vault:**
|
|
25
|
+
- **Vault:** `${VAULT_PATH}`
|
|
26
26
|
- **Config:** Read `knowledge/obsidian-config.json` for full path/convention details
|
|
27
27
|
- **MCP:** Use Obsidian MCP when available, fallback to direct file Write
|
|
28
28
|
- **Conventions:** YAML frontmatter, wikilinks `[[]]`, MOC references, kebab-case tags
|
package/installer/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ const { values, positionals } = parseArgs({
|
|
|
19
19
|
runtime: { type: "string", short: "r" },
|
|
20
20
|
path: { type: "string", short: "p" },
|
|
21
21
|
force: { type: "boolean", short: "f" },
|
|
22
|
+
"no-system": { type: "boolean" },
|
|
22
23
|
},
|
|
23
24
|
allowPositionals: true,
|
|
24
25
|
strict: false,
|
|
@@ -74,7 +75,7 @@ async function main() {
|
|
|
74
75
|
switch (command) {
|
|
75
76
|
case "install":
|
|
76
77
|
const runtime = values.runtime || await detectRuntime();
|
|
77
|
-
await install({ runtime, path: values.path, force: values.force });
|
|
78
|
+
await install({ runtime, path: values.path, force: values.force, skipSystem: values["no-system"] });
|
|
78
79
|
break;
|
|
79
80
|
|
|
80
81
|
case "init": {
|
package/installer/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { getArkaosPython, getVenvPython, canImportCore, getRepoRoot } from "./python-resolver.js";
|
|
6
6
|
import { IS_WINDOWS, HOOK_EXT, CMD_FINDER } from "./platform.js";
|
|
7
|
+
import { checkNode, checkObsidian } from "./system-tools.js";
|
|
7
8
|
|
|
8
9
|
const INSTALL_DIR = join(homedir(), ".arkaos");
|
|
9
10
|
|
|
@@ -124,6 +125,53 @@ const checks = [
|
|
|
124
125
|
check: () => existsSync(join(INSTALL_DIR, "schedules.yaml")),
|
|
125
126
|
fix: () => "Run: npx arkaos@latest update (deploys scheduler)",
|
|
126
127
|
},
|
|
128
|
+
{
|
|
129
|
+
name: "obsidian",
|
|
130
|
+
description: "Obsidian app installed",
|
|
131
|
+
severity: "warn",
|
|
132
|
+
check: () => checkObsidian().installed,
|
|
133
|
+
fix: () => {
|
|
134
|
+
const s = checkObsidian();
|
|
135
|
+
if (s.suggestedCommand) return `Run: ${s.suggestedCommand}`;
|
|
136
|
+
return `Install Obsidian from ${s.fallbackUrl || "https://obsidian.md/download"}`;
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "node",
|
|
141
|
+
description: "Node.js 20+ available",
|
|
142
|
+
severity: "warn",
|
|
143
|
+
check: () => checkNode().needsAction === "none",
|
|
144
|
+
fix: () => {
|
|
145
|
+
const s = checkNode();
|
|
146
|
+
if (s.suggestedCommand) return `Run: ${s.suggestedCommand}`;
|
|
147
|
+
return `Install Node.js 20+ from ${s.fallbackUrl || "https://nodejs.org/en/download"}`;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "claude-code-version",
|
|
152
|
+
description: "Claude Code 2.1.122+ (ToolSearch late-binding + hooks isolation)",
|
|
153
|
+
severity: "warn",
|
|
154
|
+
check: () => {
|
|
155
|
+
if (!commandExists("claude")) return true; // no claude binary = not applicable
|
|
156
|
+
try {
|
|
157
|
+
const out = execSync("claude --version 2>&1", {
|
|
158
|
+
stdio: "pipe",
|
|
159
|
+
}).toString().trim();
|
|
160
|
+
const m = out.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
161
|
+
if (!m) return false;
|
|
162
|
+
const [, maj, min, patch] = m.map(Number);
|
|
163
|
+
// 2.1.122 minimum
|
|
164
|
+
if (maj > 2) return true;
|
|
165
|
+
if (maj < 2) return false;
|
|
166
|
+
if (min > 1) return true;
|
|
167
|
+
if (min < 1) return false;
|
|
168
|
+
return patch >= 122;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
fix: () => "Upgrade Claude Code: npm install -g @anthropic-ai/claude-code@latest",
|
|
174
|
+
},
|
|
127
175
|
];
|
|
128
176
|
|
|
129
177
|
// ─── Windows-only checks ───────────────────────────────────────────────
|
package/installer/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const __dirname = dirname(__filename);
|
|
|
12
12
|
const ARKAOS_ROOT = resolve(__dirname, "..");
|
|
13
13
|
const VERSION = JSON.parse(readFileSync(join(ARKAOS_ROOT, "package.json"), "utf-8")).version;
|
|
14
14
|
|
|
15
|
-
export async function install({ runtime, path, force }) {
|
|
15
|
+
export async function install({ runtime, path, force, skipSystem }) {
|
|
16
16
|
const startTime = Date.now();
|
|
17
17
|
const config = getRuntimeConfig(runtime);
|
|
18
18
|
const isUpgrade = existsSync(join(path || join(homedir(), ".arkaos"), "install-manifest.json"));
|
|
@@ -72,6 +72,31 @@ export async function install({ runtime, path, force }) {
|
|
|
72
72
|
ok("No v1 installation found");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// ═══ Step 2.5: Ensure system tools (Obsidian, Node ≥ 20, Python ≥ 3.12) ═══
|
|
76
|
+
// Wrapped in try/catch so a regression in system-tools.js cannot break a
|
|
77
|
+
// fresh `npx arkaos install` for the ~20K user base. We degrade to a warn
|
|
78
|
+
// and proceed to venv creation rather than exiting non-zero.
|
|
79
|
+
if (!skipSystem) {
|
|
80
|
+
try {
|
|
81
|
+
const { ensureSystemTools } = await import("./system-tools.js");
|
|
82
|
+
const { formatSudoInstructions } = await import("./package-manager.js");
|
|
83
|
+
const sys = ensureSystemTools({ skipSystem: false });
|
|
84
|
+
if (sys.sudoCommands && sys.sudoCommands.length > 0) {
|
|
85
|
+
console.log(formatSudoInstructions(sys.sudoCommands));
|
|
86
|
+
}
|
|
87
|
+
for (const tool of [sys.obsidian, sys.node]) {
|
|
88
|
+
if (!tool) continue;
|
|
89
|
+
if (tool.justInstalled) ok(`${tool.name} installed`);
|
|
90
|
+
else if (tool.needsAction === "none") ok(`${tool.name} ready`);
|
|
91
|
+
else warn(`${tool.name} ${tool.needsAction} — see commands above`);
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
warn(`System tool check failed: ${err.message}. Continuing without it.`);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
warn("System tool checks skipped (--no-system)");
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
// ═══ Step 3: Check Python + create venv ═══
|
|
76
101
|
step(3, 14, "Checking Python 3.11+ and creating virtual environment...");
|
|
77
102
|
const systemPython = findSystemPython();
|
|
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
+
import { migrateProfileSchemaV3 } from "./migrations/v3_path_schema.js";
|
|
6
|
+
|
|
5
7
|
const LEGACY_SKILLS_ROOT = join(homedir(), ".claude", "skills", "arka");
|
|
6
8
|
const USER_DATA_ROOT = join(homedir(), ".arkaos");
|
|
7
9
|
|
|
@@ -28,8 +30,22 @@ export function migrateUserData({ dryRun = false } = {}) {
|
|
|
28
30
|
const skipped = [...projectsResult.skipped, ...ecosystemsResult.skipped];
|
|
29
31
|
const conflicts = [...projectsResult.conflicts, ...ecosystemsResult.conflicts];
|
|
30
32
|
|
|
33
|
+
let profileSchemaResult = null;
|
|
34
|
+
if (!dryRun) {
|
|
35
|
+
try {
|
|
36
|
+
profileSchemaResult = migrateProfileSchemaV3();
|
|
37
|
+
if (profileSchemaResult?.migrated) {
|
|
38
|
+
moved.push(
|
|
39
|
+
`profile.json schema v2 → v3 (${profileSchemaResult.projectRoots.length} roots)`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
conflicts.push(`profile schema v3 migration failed: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
const logPath = writeReport({ moved, skipped, conflicts, dryRun });
|
|
32
|
-
return { moved, skipped, conflicts, logPath };
|
|
48
|
+
return { moved, skipped, conflicts, logPath, profileSchemaResult };
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
export function printMigrationReport(result) {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: profile.json schema v2 → v3 (paths-portability, v2.23.0).
|
|
3
|
+
*
|
|
4
|
+
* Adds the structured `projectRoots: string[]` and `reposRoot: string`
|
|
5
|
+
* fields to every existing profile.json so the new path_resolver token
|
|
6
|
+
* substitution can run for the 20K-user installed base.
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Skip if `version === "3"` or `projectRoots` already present (idempotent).
|
|
10
|
+
* 2. Otherwise:
|
|
11
|
+
* - Write `.bak-<unix-timestamp>` backup beside the profile.
|
|
12
|
+
* - Parse `projectsDir` free-text to extract absolute paths.
|
|
13
|
+
* - Default to ["~/Herd","~/Work","~/AIProjects"] if parsing returns 0.
|
|
14
|
+
* - reposRoot = first match containing "AIProjects", else "~/AIProjects".
|
|
15
|
+
* - Atomic write (temp file + rename) with version: "3" and migrated_at.
|
|
16
|
+
* - Log to ~/.arkaos/logs/migrate.log.
|
|
17
|
+
*
|
|
18
|
+
* Safe for concurrent invocations: the backup filename includes the
|
|
19
|
+
* timestamp, atomic rename prevents partial writes, and the idempotent
|
|
20
|
+
* guard means a second run is a no-op.
|
|
21
|
+
*
|
|
22
|
+
* Contract documented in core/specs/SPEC-paths-portability.md (PR1 v2.23.0).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
appendFileSync,
|
|
27
|
+
existsSync,
|
|
28
|
+
mkdirSync,
|
|
29
|
+
readFileSync,
|
|
30
|
+
renameSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
} from "node:fs";
|
|
33
|
+
import { homedir } from "node:os";
|
|
34
|
+
import { dirname, join } from "node:path";
|
|
35
|
+
|
|
36
|
+
const DEFAULT_PROJECT_ROOTS = ["~/Herd", "~/Work", "~/AIProjects"];
|
|
37
|
+
const DEFAULT_REPOS_ROOT = "~/AIProjects";
|
|
38
|
+
|
|
39
|
+
export function migrateProfileSchemaV3({ profilePath, logPath } = {}) {
|
|
40
|
+
const profile = profilePath || join(homedir(), ".arkaos", "profile.json");
|
|
41
|
+
const log = logPath || join(homedir(), ".arkaos", "logs", "migrate.log");
|
|
42
|
+
|
|
43
|
+
if (!existsSync(profile)) {
|
|
44
|
+
return { skipped: true, reason: "profile.json absent" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = JSON.parse(readFileSync(profile, "utf8"));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { skipped: true, reason: `parse error: ${err.message}` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (raw.version === "3" || Array.isArray(raw.projectRoots)) {
|
|
55
|
+
return { skipped: true, reason: "already migrated" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const projectRoots = parseProjectsDirText(raw.projectsDir || "");
|
|
59
|
+
const reposRoot =
|
|
60
|
+
projectRoots.find((r) => r.includes("AIProjects")) || DEFAULT_REPOS_ROOT;
|
|
61
|
+
|
|
62
|
+
const migrated = {
|
|
63
|
+
...raw,
|
|
64
|
+
version: "3",
|
|
65
|
+
projectRoots,
|
|
66
|
+
reposRoot,
|
|
67
|
+
migrated_at: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const backup = `${profile}.bak-${Math.floor(Date.now() / 1000)}`;
|
|
71
|
+
writeFileSync(backup, readFileSync(profile, "utf8"));
|
|
72
|
+
|
|
73
|
+
const tmp = `${profile}.tmp`;
|
|
74
|
+
writeFileSync(tmp, JSON.stringify(migrated, null, 2));
|
|
75
|
+
renameSync(tmp, profile);
|
|
76
|
+
|
|
77
|
+
writeLog(log, projectRoots, reposRoot);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
migrated: true,
|
|
81
|
+
projectRoots,
|
|
82
|
+
reposRoot,
|
|
83
|
+
backup,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseProjectsDirText(text) {
|
|
88
|
+
if (!text) return [...DEFAULT_PROJECT_ROOTS];
|
|
89
|
+
const posix = /(?:\/Users|\/home)\/\S+?\/(?:Herd|Work|AIProjects|code|repos)/g;
|
|
90
|
+
const windows = /[A-Z]:\\Users\\[^\s\\]+\\(?:Herd|Work|AIProjects|code|repos)/g;
|
|
91
|
+
const found = [
|
|
92
|
+
...(text.match(posix) || []),
|
|
93
|
+
...(text.match(windows) || []),
|
|
94
|
+
];
|
|
95
|
+
return found.length > 0 ? found : [...DEFAULT_PROJECT_ROOTS];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeLog(logPath, projectRoots, reposRoot) {
|
|
99
|
+
try {
|
|
100
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
101
|
+
const line =
|
|
102
|
+
`[arka:migrated] ${new Date().toISOString()} ` +
|
|
103
|
+
`profile.json schema v2 → v3 (` +
|
|
104
|
+
`${projectRoots.length} roots, reposRoot=${reposRoot})\n`;
|
|
105
|
+
appendFileSync(logPath, line);
|
|
106
|
+
} catch {
|
|
107
|
+
// Log failure should never block migration.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-OS package manager abstraction for the ArkaOS installer.
|
|
3
|
+
*
|
|
4
|
+
* Centralises detection of brew (macOS), winget/choco (Windows) and
|
|
5
|
+
* apt/snap (Linux) so install/update flows can either run a non-sudo
|
|
6
|
+
* install themselves or return a copy-paste command for the user.
|
|
7
|
+
*
|
|
8
|
+
* **Never invokes sudo automatically.** When sudo is required (apt /
|
|
9
|
+
* snap on Linux, winget without elevation), `installViaPackageManager`
|
|
10
|
+
* returns `{ needsSudo: true, command }` and the caller is expected
|
|
11
|
+
* to print the command for the user to run. This keeps the installer
|
|
12
|
+
* headless-safe — it must not surprise a CI job with a password prompt.
|
|
13
|
+
*
|
|
14
|
+
* See core/specs/SPEC-installer-cross-os.md (PR2 v2.24.0).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { platform } from "node:os";
|
|
19
|
+
import { IS_WINDOWS } from "./platform.js";
|
|
20
|
+
|
|
21
|
+
const MAC = "darwin";
|
|
22
|
+
const LINUX = "linux";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Detect the highest-priority package manager available on this host.
|
|
26
|
+
*
|
|
27
|
+
* Priority order:
|
|
28
|
+
* macOS: brew
|
|
29
|
+
* Linux: apt > snap (apt for generic packages; snap preferred for
|
|
30
|
+
* Obsidian because its tarball is classic-confined)
|
|
31
|
+
* Windows: winget > choco
|
|
32
|
+
*
|
|
33
|
+
* Returns the canonical name string or null when nothing is available.
|
|
34
|
+
*/
|
|
35
|
+
export function detectPackageManager() {
|
|
36
|
+
const candidates = preferredCandidates();
|
|
37
|
+
for (const name of candidates) {
|
|
38
|
+
if (hasCommand(name)) return name;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return every package manager available on this host, in priority order.
|
|
45
|
+
* Useful for callers that need a different manager per package (e.g.
|
|
46
|
+
* snap for Obsidian on a host that also has apt for Node).
|
|
47
|
+
*/
|
|
48
|
+
export function detectAllPackageManagers() {
|
|
49
|
+
return preferredCandidates().filter((name) => hasCommand(name));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the install command for a package on a given manager, without
|
|
54
|
+
* executing it. Useful for `formatSudoInstructions` and for dry-run paths.
|
|
55
|
+
*/
|
|
56
|
+
export function buildInstallCommand(manager, pkg) {
|
|
57
|
+
switch (manager) {
|
|
58
|
+
case "brew":
|
|
59
|
+
return pkg.startsWith("cask:")
|
|
60
|
+
? `brew install --cask ${pkg.slice(5)}`
|
|
61
|
+
: `brew install ${pkg}`;
|
|
62
|
+
case "apt":
|
|
63
|
+
return `sudo apt update && sudo apt install -y ${pkg}`;
|
|
64
|
+
case "snap":
|
|
65
|
+
return pkg.endsWith(":classic")
|
|
66
|
+
? `sudo snap install ${pkg.slice(0, -8)} --classic`
|
|
67
|
+
: `sudo snap install ${pkg}`;
|
|
68
|
+
case "winget":
|
|
69
|
+
return `winget install --id ${pkg} --silent --accept-source-agreements --accept-package-agreements`;
|
|
70
|
+
case "choco":
|
|
71
|
+
return `choco install ${pkg} -y`;
|
|
72
|
+
default:
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether a manager needs sudo (or Windows elevation) for install.
|
|
79
|
+
* brew on macOS is intentionally not sudo-gated (Homebrew runs as the user).
|
|
80
|
+
*/
|
|
81
|
+
export function managerNeedsSudo(manager) {
|
|
82
|
+
return manager === "apt" || manager === "snap";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Attempt a non-sudo install. When the chosen manager requires sudo
|
|
87
|
+
* we never invoke it ourselves — the caller surfaces the command to
|
|
88
|
+
* the user. Returns a structured result the caller can react to.
|
|
89
|
+
*/
|
|
90
|
+
export function installViaPackageManager(pkg, options = {}) {
|
|
91
|
+
const manager = options.manager || detectPackageManager();
|
|
92
|
+
if (!manager) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
manager: null,
|
|
96
|
+
command: "",
|
|
97
|
+
needsSudo: false,
|
|
98
|
+
installed: false,
|
|
99
|
+
error: "no_package_manager",
|
|
100
|
+
fallbackUrl: options.fallbackUrl || null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const command = buildInstallCommand(manager, pkg);
|
|
105
|
+
const needsSudo = managerNeedsSudo(manager);
|
|
106
|
+
|
|
107
|
+
if (needsSudo) {
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
manager,
|
|
111
|
+
command,
|
|
112
|
+
needsSudo: true,
|
|
113
|
+
installed: false,
|
|
114
|
+
error: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.dryRun) {
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
manager,
|
|
122
|
+
command,
|
|
123
|
+
needsSudo: false,
|
|
124
|
+
installed: false,
|
|
125
|
+
error: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
execSync(command, { stdio: "inherit" });
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
manager,
|
|
134
|
+
command,
|
|
135
|
+
needsSudo: false,
|
|
136
|
+
installed: true,
|
|
137
|
+
error: null,
|
|
138
|
+
};
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
manager,
|
|
143
|
+
command,
|
|
144
|
+
needsSudo: false,
|
|
145
|
+
installed: false,
|
|
146
|
+
error: err?.message || "install_failed",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format an array of sudo install commands into a copy-paste block.
|
|
153
|
+
* Used by ensureSystemTools to print a single, friendly block when
|
|
154
|
+
* several packages need privileged install on Linux.
|
|
155
|
+
*/
|
|
156
|
+
export function formatSudoInstructions(commands) {
|
|
157
|
+
if (!commands || commands.length === 0) return "";
|
|
158
|
+
const unique = [...new Set(commands.filter(Boolean))];
|
|
159
|
+
if (unique.length === 0) return "";
|
|
160
|
+
const lines = [
|
|
161
|
+
"",
|
|
162
|
+
" To finish setup, please run these commands:",
|
|
163
|
+
"",
|
|
164
|
+
...unique.map((c) => ` ${c}`),
|
|
165
|
+
"",
|
|
166
|
+
" Then re-run: npx arkaos install",
|
|
167
|
+
"",
|
|
168
|
+
];
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function preferredCandidates() {
|
|
173
|
+
const os = platform();
|
|
174
|
+
if (os === MAC) return ["brew"];
|
|
175
|
+
if (os === LINUX) return ["apt", "snap"];
|
|
176
|
+
if (IS_WINDOWS) return ["winget", "choco"];
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasCommand(name) {
|
|
181
|
+
const finder = IS_WINDOWS ? "where" : "command -v";
|
|
182
|
+
try {
|
|
183
|
+
execSync(`${finder} ${name}`, {
|
|
184
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
185
|
+
shell: !IS_WINDOWS,
|
|
186
|
+
});
|
|
187
|
+
return true;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|