@amoree-code/ocean 0.0.0-stage → 1.2.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/LICENSE +21 -0
- package/README.md +284 -2
- package/dist/application/auth/auth-orchestrator.js +77 -0
- package/dist/application/clients/clients.js +646 -0
- package/dist/application/clients/editors.js +398 -0
- package/dist/application/clients/hooks.js +335 -0
- package/dist/application/clients/native.js +410 -0
- package/dist/application/clients/opencode-plugin.js +158 -0
- package/dist/application/clients/registry.js +188 -0
- package/dist/application/gateway/webhook-gateway.js +137 -0
- package/dist/application/install/provider-installer.js +184 -0
- package/dist/application/integrations/charter-copies.js +76 -0
- package/dist/application/integrations/claude-vscode.js +106 -0
- package/dist/application/integrations/engine-entry-repair.js +105 -0
- package/dist/application/integrations/skill-filter.js +254 -0
- package/dist/application/layout/layout-apply.js +443 -0
- package/dist/application/layout/layout-plan.js +236 -0
- package/dist/application/layout/workspace-template.js +34 -0
- package/dist/application/onboarding/state.js +23 -0
- package/dist/application/onboarding/steps.js +249 -0
- package/dist/application/profiles/profile-distribution.js +53 -0
- package/dist/application/profiles/profile-suggestion.js +39 -0
- package/dist/application/profiles/profile-upgrade-service.js +208 -0
- package/dist/application/pull/history-index.js +476 -0
- package/dist/application/pull/project-match.js +132 -0
- package/dist/application/pull/pull.js +202 -0
- package/dist/application/pull/server-capture.js +307 -0
- package/dist/application/scheduler/local-scheduler.js +210 -0
- package/dist/composition/runtime.js +64 -0
- package/dist/engine-root.js +9 -0
- package/dist/infrastructure/filesystem/profile-loader.js +46 -0
- package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
- package/dist/infrastructure/filesystem/skill-loader.js +84 -0
- package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
- package/dist/infrastructure/observability/runtime-logger.js +16 -0
- package/dist/infrastructure/process/agent-tool-executor.js +99 -0
- package/dist/infrastructure/process/cli-process.js +49 -0
- package/dist/infrastructure/process/interactive-process.js +145 -0
- package/dist/infrastructure/process/process-supervisor.js +194 -0
- package/dist/infrastructure/process/service.js +252 -0
- package/dist/infrastructure/providers/browser-provider.js +2 -0
- package/dist/infrastructure/providers/claude-sdk.js +137 -0
- package/dist/infrastructure/providers/client-home.js +14 -0
- package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
- package/dist/infrastructure/providers/ollama-embedder.js +74 -0
- package/dist/infrastructure/providers/openai-compatible.js +268 -0
- package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
- package/dist/infrastructure/providers/provider-capabilities.js +50 -0
- package/dist/infrastructure/providers/provider-invocation.js +184 -0
- package/dist/infrastructure/providers/provider-registry.js +77 -0
- package/dist/infrastructure/providers/providers.js +134 -0
- package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
- package/dist/interfaces/cli/auth-command.js +54 -0
- package/dist/interfaces/cli/browser-bridge-command.js +93 -0
- package/dist/interfaces/cli/browser-command.js +119 -0
- package/dist/interfaces/cli/capture-command.js +125 -0
- package/dist/interfaces/cli/client-test-command.js +133 -0
- package/dist/interfaces/cli/clients-command.js +129 -0
- package/dist/interfaces/cli/command-registry.js +1211 -0
- package/dist/interfaces/cli/context-command.js +172 -0
- package/dist/interfaces/cli/daily-command.js +131 -0
- package/dist/interfaces/cli/governance-command.js +55 -0
- package/dist/interfaces/cli/handoff-command.js +35 -0
- package/dist/interfaces/cli/help-command.js +207 -0
- package/dist/interfaces/cli/idea-command.js +47 -0
- package/dist/interfaces/cli/intercept-command.js +317 -0
- package/dist/interfaces/cli/layout-command.js +27 -0
- package/dist/interfaces/cli/market-command.js +52 -0
- package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
- package/dist/interfaces/cli/memory-command.js +317 -0
- package/dist/interfaces/cli/migrate-command.js +26 -0
- package/dist/interfaces/cli/observe-command.js +78 -0
- package/dist/interfaces/cli/onboard-command.js +123 -0
- package/dist/interfaces/cli/operate-command.js +59 -0
- package/dist/interfaces/cli/profiles-command.js +70 -0
- package/dist/interfaces/cli/pull-command.js +54 -0
- package/dist/interfaces/cli/setup-command.js +143 -0
- package/dist/interfaces/cli/tasks-command.js +249 -0
- package/dist/main.js +49 -0
- package/dist/node-warnings.js +17 -0
- package/dist/version.js +8 -0
- package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
- package/market/mcp/gateway/ocean.capability.json +21 -0
- package/market/mcp/obsidian/ocean.capability.json +21 -0
- package/market/mcp/ocean/ocean.capability.json +21 -0
- package/market/mcp/playwright/ocean.capability.json +23 -0
- package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
- package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
- package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
- package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
- package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
- package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
- package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
- package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
- package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
- package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
- package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
- package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
- package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
- package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
- package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
- package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
- package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
- package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
- package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
- package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
- package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
- package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
- package/node_modules/@ocean/brain/package.json +11 -0
- package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
- package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
- package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
- package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
- package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
- package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
- package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
- package/node_modules/@ocean/context/package.json +11 -0
- package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
- package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
- package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
- package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
- package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
- package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
- package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
- package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
- package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
- package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
- package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
- package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
- package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
- package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
- package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
- package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
- package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
- package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
- package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
- package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
- package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
- package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
- package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
- package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
- package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
- package/node_modules/@ocean/core/package.json +14 -0
- package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
- package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
- package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
- package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
- package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
- package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
- package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
- package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
- package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
- package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
- package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
- package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
- package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
- package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
- package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
- package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
- package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
- package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
- package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
- package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
- package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
- package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
- package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
- package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
- package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
- package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
- package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
- package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
- package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
- package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
- package/node_modules/@ocean/loop/package.json +11 -0
- package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
- package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
- package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
- package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
- package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
- package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
- package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
- package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
- package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
- package/node_modules/@ocean/market/dist/install/lock.js +34 -0
- package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
- package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
- package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
- package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
- package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
- package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
- package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
- package/node_modules/@ocean/market/package.json +11 -0
- package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
- package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
- package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
- package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
- package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
- package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
- package/node_modules/@ocean/services/package.json +11 -0
- package/ocean-marketplace.json +415 -0
- package/package.json +67 -4
- package/skills/core/business-logic/SKILL.md +22 -0
- package/skills/core/catch-up/SKILL.md +17 -0
- package/skills/core/core-thinking/SKILL.md +11 -0
- package/skills/core/design-thinking/SKILL.md +19 -0
- package/skills/core/graft/SKILL.md +156 -0
- package/skills/core/loop/SKILL.md +20 -0
- package/skills/core/session-handoff/SKILL.md +15 -0
- package/skills/core/use-browser/SKILL.md +69 -0
- package/skills/core/verification/SKILL.md +11 -0
- package/skills/index.json +56 -0
- package/templates/README.md +4 -0
- package/templates/profiles/default.json +51 -0
- package/templates/profiles/developer.json +51 -0
- package/templates/profiles/reviewer.json +51 -0
- package/templates/profiles/strategist.json +51 -0
- package/templates/workspace/00-inbox/INBOX.md +7 -0
- package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
- package/templates/workspace/00-inbox/native/.gitkeep +0 -0
- package/templates/workspace/01-daily/.gitkeep +0 -0
- package/templates/workspace/02-personal/CONFLICTS.md +7 -0
- package/templates/workspace/02-personal/MEMORY.md +15 -0
- package/templates/workspace/02-personal/goals/.gitkeep +0 -0
- package/templates/workspace/02-personal/identity/.gitkeep +0 -0
- package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
- package/templates/workspace/03-professional/.gitkeep +0 -0
- package/templates/workspace/04-projects/PROJECTS.md +5 -0
- package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
- package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
- package/templates/workspace/06-templates/TEMPLATES.md +25 -0
- package/templates/workspace/06-templates/decision.md +8 -0
- package/templates/workspace/06-templates/intelligence.md +32 -0
- package/templates/workspace/06-templates/project-claude.md +48 -0
- package/templates/workspace/06-templates/session.md +38 -0
- package/templates/workspace/06-templates/task.md +42 -0
- package/templates/workspace/MANIFEST.json +51 -0
- package/templates/workspace/bridge/config/CONFIG.md +21 -0
- package/templates/workspace/bridge/config/runtime.json +4 -0
- package/templates/workspace/charter/core.md +104 -0
- package/templates/workspace/charter/policies/README.md +31 -0
- package/templates/workspace/charter/policies/claude-code-context.md +90 -0
- package/templates/workspace/charter/policies/concise-output.md +29 -0
- package/templates/workspace/charter/policies/context.md +91 -0
- package/templates/workspace/charter/policies/contracts/task.md +189 -0
- package/templates/workspace/charter/policies/git.md +60 -0
- package/templates/workspace/charter/policies/graph.md +47 -0
- package/templates/workspace/charter/policies/knowledge.md +33 -0
- package/templates/workspace/charter/policies/lifecycle.md +119 -0
- package/templates/workspace/charter/policies/memory.md +67 -0
- package/templates/workspace/charter/policies/minimal-change.md +43 -0
- package/templates/workspace/charter/policies/models.md +57 -0
- package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
- package/templates/workspace/charter/policies/response.md +39 -0
- package/templates/workspace/charter/policies/skill-observer.md +44 -0
- package/templates/workspace/charter/policies/stack.md +23 -0
- package/templates/workspace/charter/policies/task.md +139 -0
- package/templates/workspace/charter/policies/verification.md +29 -0
- package/templates/workspace/sessions/.gitkeep +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { oceanRoot } from "@ocean/core/shared/paths.js";
|
|
5
|
+
// The workspace template shipped in the package (T-296 §3.9): generic charter, policies,
|
|
6
|
+
// templates and indexes, empty private files, and the required empty directories. Its classes
|
|
7
|
+
// live in MANIFEST.json and are checked by scripts/check-template-purity.mjs.
|
|
8
|
+
export const WORKSPACE_TEMPLATE = fileURLToPath(new URL("../../../templates/workspace/", import.meta.url));
|
|
9
|
+
// Copies every template file the workspace does not have yet and creates the required
|
|
10
|
+
// directories. A file that already exists is never replaced, so a rerun on a populated
|
|
11
|
+
// workspace changes nothing. `.gitkeep` files are not copied: they only keep the template's
|
|
12
|
+
// empty directories in git.
|
|
13
|
+
export async function seedWorkspaceTemplate(root = oceanRoot(), template = WORKSPACE_TEMPLATE) {
|
|
14
|
+
const manifest = JSON.parse(await readFile(path.join(template, "MANIFEST.json"), "utf8"));
|
|
15
|
+
const created = [];
|
|
16
|
+
const kept = [];
|
|
17
|
+
for (const [file, kind] of Object.entries(manifest.files)) {
|
|
18
|
+
const target = path.join(root, ...file.split("/"));
|
|
19
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
20
|
+
if (kind === "keep")
|
|
21
|
+
continue;
|
|
22
|
+
try {
|
|
23
|
+
await writeFile(target, await readFile(path.join(template, ...file.split("/"))), { flag: "wx" });
|
|
24
|
+
created.push(file);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code !== "EEXIST")
|
|
28
|
+
throw error;
|
|
29
|
+
kept.push(file);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { created, kept };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=workspace-template.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
4
|
+
export function onboardingStatePath() {
|
|
5
|
+
return oceanPath(SYSTEM_DIR, "state", "onboarding.json");
|
|
6
|
+
}
|
|
7
|
+
export async function loadOnboardingState() {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(await readFile(onboardingStatePath(), "utf8"));
|
|
10
|
+
return { version: parsed.version ?? 0, steps: parsed.steps ?? {} };
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return { version: 0, steps: {} };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function saveOnboardingState(state) {
|
|
17
|
+
const file = onboardingStatePath();
|
|
18
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
19
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
20
|
+
await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`);
|
|
21
|
+
await rename(temporary, file);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { oceanPath, PERSONAL_DIR } from "@ocean/core/shared/paths.js";
|
|
3
|
+
import { copyClientSkills, populateSkillHub, } from "@ocean/market/skills/skill-hub.js";
|
|
4
|
+
import { connectObsidianVault } from "@ocean/services/obsidian/vault-discovery.js";
|
|
5
|
+
// Bump when a step is added; completed steps stay completed, only new ids run.
|
|
6
|
+
export const ONBOARDING_VERSION = 3;
|
|
7
|
+
async function yes(io, question) {
|
|
8
|
+
return (await io.ask(`${question} [Y/n] `, "y")).trim().toLowerCase() !== "n";
|
|
9
|
+
}
|
|
10
|
+
function describeClient(plan) {
|
|
11
|
+
if (plan.status === "current")
|
|
12
|
+
return "connected";
|
|
13
|
+
if (plan.status === "change")
|
|
14
|
+
return `will ${plan.ops.map((op) => `${op.kind} ${op.key}`).join(", ")} in ${plan.file}`;
|
|
15
|
+
if (plan.status === "manual")
|
|
16
|
+
return `hand edit needed (${plan.reason})`;
|
|
17
|
+
if (plan.status === "unknown")
|
|
18
|
+
return `unknown: ${plan.reason}`;
|
|
19
|
+
return `${plan.status}: ${plan.reason}; fallback: ${plan.fallback}`;
|
|
20
|
+
}
|
|
21
|
+
// T-296 §3.7: one consolidated review of every detected client (MCP entry and session hook), one
|
|
22
|
+
// confirmation for all of them. Without a terminal nothing is written: client configs change
|
|
23
|
+
// only on an explicit yes.
|
|
24
|
+
function connectStep(clients) {
|
|
25
|
+
return {
|
|
26
|
+
id: "connect",
|
|
27
|
+
title: "Connect AI clients to Ocean (one review, one apply)",
|
|
28
|
+
optional: false,
|
|
29
|
+
// Drift of an approved client is repaired by `client sync`; only a new one needs a review.
|
|
30
|
+
async reopen() {
|
|
31
|
+
const approved = new Set(await clients.approved());
|
|
32
|
+
const fresh = (await clients.plan())
|
|
33
|
+
.filter((plan) => plan.installed &&
|
|
34
|
+
plan.status === "change" &&
|
|
35
|
+
!approved.has(plan.client))
|
|
36
|
+
.map((plan) => plan.client);
|
|
37
|
+
return fresh.length
|
|
38
|
+
? `new client(s) to connect: ${fresh.join(", ")}`
|
|
39
|
+
: null;
|
|
40
|
+
},
|
|
41
|
+
async run(io) {
|
|
42
|
+
const plans = (await clients.plan()).filter((plan) => plan.installed);
|
|
43
|
+
for (const plan of plans)
|
|
44
|
+
io.say(` ${plan.label.padEnd(34)} ${describeClient(plan)}`);
|
|
45
|
+
for (const plan of plans.filter((item) => item.status === "manual"))
|
|
46
|
+
io.say(` Hand edit for ${plan.label} (${plan.file}): add ${plan.snippet}`);
|
|
47
|
+
const hooks = await clients.hooks();
|
|
48
|
+
for (const hook of hooks)
|
|
49
|
+
io.say(` ${`${hook.client} ${hook.event} hook`.padEnd(34)} ${hook.status === "current"
|
|
50
|
+
? "present"
|
|
51
|
+
: hook.status === "change"
|
|
52
|
+
? `will add a ${hook.event} hook in ${hook.file}`
|
|
53
|
+
: `hand edit needed (${hook.reason})${hook.snippet ? `; add ${hook.snippet}` : ""}`}`);
|
|
54
|
+
const native = await clients.native();
|
|
55
|
+
for (const item of native)
|
|
56
|
+
io.say(` ${`${item.client} native memory`.padEnd(34)} ${item.status === "current"
|
|
57
|
+
? "governed"
|
|
58
|
+
: item.status === "change"
|
|
59
|
+
? `will set ${item.setting} in ${item.file}`
|
|
60
|
+
: item.status === "manual"
|
|
61
|
+
? `hand edit: ${item.setting} in ${item.file} (${item.reason})`
|
|
62
|
+
: item.status === "capture"
|
|
63
|
+
? `captured: ${item.reason}`
|
|
64
|
+
: `you: ${item.reason}`}`);
|
|
65
|
+
const pending = plans.filter((plan) => plan.status === "change");
|
|
66
|
+
const pendingHooks = hooks.filter((hook) => hook.status === "change");
|
|
67
|
+
const pendingNative = native.filter((item) => item.status === "change");
|
|
68
|
+
if (pending.length + pendingHooks.length + pendingNative.length === 0) {
|
|
69
|
+
io.say(" Every client Ocean can wire is already connected.");
|
|
70
|
+
return "done";
|
|
71
|
+
}
|
|
72
|
+
if (!io.interactive) {
|
|
73
|
+
io.say(" Review: ocean clients plan Apply: ocean clients apply --yes");
|
|
74
|
+
return "skipped";
|
|
75
|
+
}
|
|
76
|
+
if (!(await yes(io, ` Apply these changes (${pending.length} MCP entries, ${pendingHooks.length} session hooks, ${pendingNative.length} native-memory settings)?`)))
|
|
77
|
+
return "skipped";
|
|
78
|
+
const { batch, results } = await clients.apply(pending.map((plan) => plan.client));
|
|
79
|
+
for (const { client, event, result } of await clients.applyHooks(batch))
|
|
80
|
+
io.say(` ${client} ${event} hook: ${result}`);
|
|
81
|
+
for (const { client, result } of await clients.applyNative(batch))
|
|
82
|
+
io.say(` ${client} native memory: ${result}`);
|
|
83
|
+
io.say(` Undo with: ocean clients restore --batch ${batch}`);
|
|
84
|
+
for (const { client, result } of results) {
|
|
85
|
+
if (result !== "written") {
|
|
86
|
+
io.say(` ${client}: ${result}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const check = await clients.verify(client);
|
|
90
|
+
io.say(` ${client}: ${check.ok ? "PROVEN" : "NOT READY"} — ${check.detail}`);
|
|
91
|
+
}
|
|
92
|
+
io.say(' In-app check (you): restart each client, then ask it to call `ocean_ping` and `brain_search` for "Ocean".');
|
|
93
|
+
return "done";
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// A guided step prints the exact command instead of running it: these touch client
|
|
98
|
+
// configs, credentials or the user's own settings, which Ocean must not edit unasked.
|
|
99
|
+
function guided(id, title, lines, optional = true) {
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
title,
|
|
103
|
+
optional,
|
|
104
|
+
async run(io) {
|
|
105
|
+
lines.forEach((line) => {
|
|
106
|
+
io.say(` ${line}`);
|
|
107
|
+
});
|
|
108
|
+
return (await yes(io, " Done, or will do it later?"))
|
|
109
|
+
? "done"
|
|
110
|
+
: "skipped";
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function buildSteps(deps) {
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
id: "workspace",
|
|
118
|
+
title: "Workspace (folders, default profile, permissions)",
|
|
119
|
+
optional: false,
|
|
120
|
+
async run() {
|
|
121
|
+
await deps.setup();
|
|
122
|
+
return "done";
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "identity",
|
|
127
|
+
title: "Identity (who Ocean works for)",
|
|
128
|
+
optional: true,
|
|
129
|
+
async run(io) {
|
|
130
|
+
const name = (await io.ask(" Your name: ", "")).trim();
|
|
131
|
+
const role = (await io.ask(" Your role: ", "")).trim();
|
|
132
|
+
const language = (await io.ask(" Preferred language: ", "")).trim();
|
|
133
|
+
if (!name && !role && !language)
|
|
134
|
+
return "skipped";
|
|
135
|
+
await writeFile(oceanPath(PERSONAL_DIR, "identity.md"), `# Identity\n\n- Name: ${name}\n- Role: ${role}\n- Language: ${language}\n`, { flag: "wx" }).catch((error) => {
|
|
136
|
+
if (error.code !== "EEXIST")
|
|
137
|
+
throw error;
|
|
138
|
+
io.say(" identity.md already exists; left unchanged.");
|
|
139
|
+
});
|
|
140
|
+
return "done";
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "clients",
|
|
145
|
+
title: "AI clients (which CLIs Ocean observes)",
|
|
146
|
+
optional: false,
|
|
147
|
+
async run(io) {
|
|
148
|
+
for (const client of await deps.wrappers.wrapperStatus()) {
|
|
149
|
+
const state = client.enabled ? "enabled" : "disabled";
|
|
150
|
+
io.say(` ${client.id.padEnd(12)} ${state}`);
|
|
151
|
+
if (!io.interactive)
|
|
152
|
+
continue;
|
|
153
|
+
const enable = await yes(io, ` Observe ${client.id}?`);
|
|
154
|
+
if (enable !== client.enabled)
|
|
155
|
+
await deps.wrappers.setProviderEnabled(client.id, enable);
|
|
156
|
+
}
|
|
157
|
+
await deps.wrappers.syncProviderWrappers();
|
|
158
|
+
return "done";
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "skills",
|
|
163
|
+
title: "Skills (one store in Ocean, clients hold copies)",
|
|
164
|
+
optional: false,
|
|
165
|
+
async run(io) {
|
|
166
|
+
const hub = await populateSkillHub();
|
|
167
|
+
io.say(` Hub: ${hub.added.length} added, ${hub.kept.length} already there.`);
|
|
168
|
+
const plan = await copyClientSkills({});
|
|
169
|
+
const pending = plan.actions.filter((a) => a.action !== "synchronized");
|
|
170
|
+
if (pending.length === 0) {
|
|
171
|
+
io.say(" Clients already hold a copy of every hub skill.");
|
|
172
|
+
return "done";
|
|
173
|
+
}
|
|
174
|
+
io.say(` ${pending.length} client skill copies are out of date (originals are backed up).`);
|
|
175
|
+
if (!io.interactive) {
|
|
176
|
+
io.say(" Run: ocean skill copy --apply");
|
|
177
|
+
return "skipped";
|
|
178
|
+
}
|
|
179
|
+
if (!(await yes(io, " Apply now?")))
|
|
180
|
+
return "skipped";
|
|
181
|
+
const applied = await copyClientSkills({ apply: true });
|
|
182
|
+
io.say(` Copied. Backup: ${applied.backup ?? "none needed"}`);
|
|
183
|
+
return "done";
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "shell",
|
|
188
|
+
title: "Shell integration",
|
|
189
|
+
optional: false,
|
|
190
|
+
async run(io) {
|
|
191
|
+
io.say(` Shim directory on PATH via ${await deps.wrappers.installShellIntegration()}`);
|
|
192
|
+
return "done";
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
connectStep(deps.clients),
|
|
196
|
+
guided("auth", "Provider authentication", [
|
|
197
|
+
"Run: ocean auth",
|
|
198
|
+
"Credentials stay in the OS keychain or environment, never in files.",
|
|
199
|
+
]),
|
|
200
|
+
guided("browser", "Browser capability", ["Run: ocean browser"]),
|
|
201
|
+
{
|
|
202
|
+
id: "obsidian",
|
|
203
|
+
title: "Obsidian vault",
|
|
204
|
+
optional: true,
|
|
205
|
+
async run(io) {
|
|
206
|
+
const vault = (await io.ask(" Vault path (blank to skip): ", "")).trim();
|
|
207
|
+
if (!vault)
|
|
208
|
+
return "skipped";
|
|
209
|
+
const result = await connectObsidianVault(vault, "read-only");
|
|
210
|
+
io.say(` Connected ${result.vaultPath} (${result.noteCount} notes)`);
|
|
211
|
+
return "done";
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
guided("hooks", "SessionStart auto-sync hook", [
|
|
215
|
+
"Add before the exec in your session-bootstrap hook:",
|
|
216
|
+
'node "$HOME/ocean/kernel/packages/harness/dist/main.js" client sync >/dev/null 2>&1 &',
|
|
217
|
+
]),
|
|
218
|
+
guided("service", "Background service and schedules", [
|
|
219
|
+
"Run: ocean service (and ocean schedule for recurring jobs)",
|
|
220
|
+
]),
|
|
221
|
+
{
|
|
222
|
+
id: "verify",
|
|
223
|
+
title: "Verify",
|
|
224
|
+
optional: false,
|
|
225
|
+
async run(io) {
|
|
226
|
+
const findings = await deps.wrappers.wrapperDoctor();
|
|
227
|
+
// PATH is read from this process, which predates the profile edit made in the
|
|
228
|
+
// shell step, so that one finding only means "open a new terminal".
|
|
229
|
+
const pathNote = findings.filter((finding) => finding.includes("not on PATH"));
|
|
230
|
+
const blocking = findings.filter((finding) => !pathNote.includes(finding));
|
|
231
|
+
pathNote.forEach(() => {
|
|
232
|
+
io.say(" Open a new terminal so the shim directory is on PATH.");
|
|
233
|
+
});
|
|
234
|
+
blocking.forEach((finding) => {
|
|
235
|
+
io.say(` NOT READY: ${finding}`);
|
|
236
|
+
});
|
|
237
|
+
if (findings.length === 0)
|
|
238
|
+
io.say(" PROVEN: wrappers configured, binaries resolve outside the shim directory.");
|
|
239
|
+
if (blocking.length > 0) {
|
|
240
|
+
io.say(" Fix the above, then run: ocean onboard --reset verify");
|
|
241
|
+
return "skipped";
|
|
242
|
+
}
|
|
243
|
+
io.say(" Also run: ocean doctor");
|
|
244
|
+
return "done";
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=steps.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { validateProfileDistribution, } from "@ocean/core/domain/profiles/profile-distribution.js";
|
|
4
|
+
const forbidden = new Set([
|
|
5
|
+
".env",
|
|
6
|
+
".env.example",
|
|
7
|
+
"auth.json",
|
|
8
|
+
"memories",
|
|
9
|
+
"sessions",
|
|
10
|
+
"logs",
|
|
11
|
+
"workspace",
|
|
12
|
+
"home",
|
|
13
|
+
"state.db",
|
|
14
|
+
"state.db-shm",
|
|
15
|
+
"state.db-wal",
|
|
16
|
+
]);
|
|
17
|
+
export async function loadProfileDistribution(root) {
|
|
18
|
+
const manifest = JSON.parse(await readFile(path.join(root, "distribution.yaml"), "utf8"));
|
|
19
|
+
const distribution = validateProfileDistribution(manifest);
|
|
20
|
+
const files = distribution.files.length
|
|
21
|
+
? distribution.files
|
|
22
|
+
: await listFiles(root);
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
if (isForbidden(file))
|
|
25
|
+
throw new Error(`Profile distribution contains forbidden private state: ${file}`);
|
|
26
|
+
const resolved = path.resolve(root, file);
|
|
27
|
+
const rootPath = path.resolve(root);
|
|
28
|
+
if (resolved !== rootPath &&
|
|
29
|
+
!resolved.startsWith(`${rootPath}${path.sep}`)) {
|
|
30
|
+
throw new Error(`Profile distribution file escapes package root: ${file}`);
|
|
31
|
+
}
|
|
32
|
+
await stat(resolved);
|
|
33
|
+
}
|
|
34
|
+
return { ...distribution, files };
|
|
35
|
+
}
|
|
36
|
+
function isForbidden(file) {
|
|
37
|
+
return file
|
|
38
|
+
.split(/[\\/]/)
|
|
39
|
+
.some((part) => forbidden.has(part) || part.startsWith(".env"));
|
|
40
|
+
}
|
|
41
|
+
async function listFiles(root, relative = "") {
|
|
42
|
+
const directory = path.join(root, relative);
|
|
43
|
+
const result = [];
|
|
44
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
45
|
+
const child = path.join(relative, entry.name);
|
|
46
|
+
if (entry.isDirectory())
|
|
47
|
+
result.push(...(await listFiles(root, child)));
|
|
48
|
+
else
|
|
49
|
+
result.push(child);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=profile-distribution.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { appendFile, mkdir, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { suggestProfile, } from "@ocean/core/domain/profiles/profile-selection.js";
|
|
4
|
+
import { parseFrontmatter } from "@ocean/core/shared/fs-utils.js";
|
|
5
|
+
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
6
|
+
import { resolveTaskReference } from "@ocean/core/shared/task-reference.js";
|
|
7
|
+
function objective(body) {
|
|
8
|
+
const match = /^## Objective\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m.exec(body);
|
|
9
|
+
return match ? match[1].trim() : "";
|
|
10
|
+
}
|
|
11
|
+
async function installedProfiles() {
|
|
12
|
+
return (await readdir(oceanPath(SYSTEM_DIR, "profiles")).catch(() => []))
|
|
13
|
+
.filter((name) => name.endsWith(".json"))
|
|
14
|
+
.map((name) => name.slice(0, -".json".length));
|
|
15
|
+
}
|
|
16
|
+
export async function selectProfileForTask(ref, options = {}) {
|
|
17
|
+
const location = await resolveTaskReference(ref, { cwd: options.cwd });
|
|
18
|
+
const source = await readFile(location.file, "utf8");
|
|
19
|
+
const fields = parseFrontmatter(source);
|
|
20
|
+
const suggested = suggestProfile({
|
|
21
|
+
id: location.id,
|
|
22
|
+
title: fields.title ?? "",
|
|
23
|
+
kind: fields.kind ?? fields.type,
|
|
24
|
+
class: fields.class,
|
|
25
|
+
risk: fields.risk,
|
|
26
|
+
objective: objective(source),
|
|
27
|
+
}, await installedProfiles());
|
|
28
|
+
const selection = {
|
|
29
|
+
task: location.id,
|
|
30
|
+
profile: options.override ?? suggested.profile,
|
|
31
|
+
override: Boolean(options.override),
|
|
32
|
+
suggested,
|
|
33
|
+
};
|
|
34
|
+
const log = oceanPath(SYSTEM_DIR, "state", "profiles", "selections.jsonl");
|
|
35
|
+
await mkdir(path.dirname(log), { recursive: true, mode: 0o700 });
|
|
36
|
+
await appendFile(log, `${JSON.stringify({ ts: new Date().toISOString(), ...selection })}\n`, { mode: 0o600 });
|
|
37
|
+
return selection;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=profile-suggestion.js.map
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { upgradeProfile } from "@ocean/core/domain/profiles/profile-upgrade.js";
|
|
5
|
+
import { atomicWrite, withFileLock } from "@ocean/core/shared/fs-utils.js";
|
|
6
|
+
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
7
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
8
|
+
export function profilesDir() {
|
|
9
|
+
return oceanPath(SYSTEM_DIR, "profiles");
|
|
10
|
+
}
|
|
11
|
+
// Backups and the manifest live beside the profiles, under a name no profile can have.
|
|
12
|
+
function upgradeDir() {
|
|
13
|
+
return path.join(profilesDir(), ".upgrade");
|
|
14
|
+
}
|
|
15
|
+
function manifestFile() {
|
|
16
|
+
return path.join(upgradeDir(), "manifest.json");
|
|
17
|
+
}
|
|
18
|
+
async function readManifest() {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(await readFile(manifestFile(), "utf8"));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === "ENOENT")
|
|
24
|
+
return null;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function isDirectory(target) {
|
|
29
|
+
return (await stat(target).catch(() => null))?.isDirectory() ?? false;
|
|
30
|
+
}
|
|
31
|
+
export async function planProfileUpgrade() {
|
|
32
|
+
const dir = profilesDir();
|
|
33
|
+
const names = (await readdir(dir).catch(() => [])).sort();
|
|
34
|
+
const entries = [];
|
|
35
|
+
for (const entry of names) {
|
|
36
|
+
const full = path.join(dir, entry);
|
|
37
|
+
if (entry.startsWith("."))
|
|
38
|
+
continue;
|
|
39
|
+
if (entry.endsWith(".json")) {
|
|
40
|
+
const name = entry.slice(0, -".json".length);
|
|
41
|
+
if (await isDirectory(path.join(dir, name))) {
|
|
42
|
+
entries.push({
|
|
43
|
+
name,
|
|
44
|
+
file: full,
|
|
45
|
+
status: "collision",
|
|
46
|
+
issues: [`both ${entry} and the legacy directory ${name}/ exist`],
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const source = await readFile(full, "utf8");
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = JSON.parse(source);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// The parser's message quotes the file around the error; it is not repeated.
|
|
57
|
+
entries.push({
|
|
58
|
+
name,
|
|
59
|
+
file: full,
|
|
60
|
+
status: "rejected",
|
|
61
|
+
issues: ["invalid JSON"],
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const result = upgradeProfile(raw);
|
|
66
|
+
const declared = raw?.name;
|
|
67
|
+
if (result.status !== "rejected" && declared !== name) {
|
|
68
|
+
entries.push({
|
|
69
|
+
name,
|
|
70
|
+
file: full,
|
|
71
|
+
status: "rejected",
|
|
72
|
+
issues: [`declares name "${String(declared)}", expected "${name}"`],
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (result.status === "upgrade") {
|
|
77
|
+
const next = `${JSON.stringify(result.profile, null, 2)}\n`;
|
|
78
|
+
entries.push({
|
|
79
|
+
name,
|
|
80
|
+
file: full,
|
|
81
|
+
status: "upgrade",
|
|
82
|
+
changes: result.changes,
|
|
83
|
+
v1Sha256: sha256(source),
|
|
84
|
+
v2Sha256: sha256(next),
|
|
85
|
+
next,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else if (result.status === "rejected")
|
|
89
|
+
entries.push({
|
|
90
|
+
name,
|
|
91
|
+
file: full,
|
|
92
|
+
status: "rejected",
|
|
93
|
+
issues: result.issues,
|
|
94
|
+
});
|
|
95
|
+
else
|
|
96
|
+
entries.push({ name, file: full, status: "current" });
|
|
97
|
+
}
|
|
98
|
+
else if ((await isDirectory(full)) && !names.includes(`${entry}.json`))
|
|
99
|
+
entries.push({
|
|
100
|
+
name: entry,
|
|
101
|
+
file: full,
|
|
102
|
+
status: "legacy-directory",
|
|
103
|
+
issues: ["legacy directory profiles are not upgraded; convert by hand"],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
// All or nothing: one rejected file or collision blocks every write. A re-run after a
|
|
109
|
+
// successful apply finds every profile current and writes nothing.
|
|
110
|
+
export async function applyProfileUpgrade() {
|
|
111
|
+
return withFileLock(manifestFile(), async () => {
|
|
112
|
+
const plan = await planProfileUpgrade();
|
|
113
|
+
const blocked = plan.filter((entry) => entry.status === "rejected" || entry.status === "collision");
|
|
114
|
+
if (blocked.length)
|
|
115
|
+
return { applied: [], blocked };
|
|
116
|
+
const pending = plan.filter((entry) => entry.status === "upgrade");
|
|
117
|
+
if (!pending.length)
|
|
118
|
+
return { applied: [], blocked: [] };
|
|
119
|
+
await mkdir(upgradeDir(), { recursive: true, mode: 0o700 });
|
|
120
|
+
// One manifest across every apply: a later batch adds to it, so rollback still covers
|
|
121
|
+
// profiles upgraded earlier. A profile upgraded again (after a rollback) replaces its entry.
|
|
122
|
+
const previous = await readManifest();
|
|
123
|
+
const manifest = {
|
|
124
|
+
appliedAt: new Date().toISOString(),
|
|
125
|
+
entries: (previous?.entries ?? []).filter((entry) => !pending.some((item) => item.name === entry.name)),
|
|
126
|
+
};
|
|
127
|
+
// Every v1 copy is written and verified before any profile is replaced.
|
|
128
|
+
for (const entry of pending) {
|
|
129
|
+
const source = await readFile(entry.file, "utf8");
|
|
130
|
+
if (sha256(source) !== entry.v1Sha256)
|
|
131
|
+
throw new Error(`${entry.name} changed during the upgrade; nothing applied`);
|
|
132
|
+
const backup = path.join(upgradeDir(), `${entry.name}.${entry.v1Sha256?.slice(0, 12)}.v1.json`);
|
|
133
|
+
await atomicWrite(backup, source, 0o600);
|
|
134
|
+
if (sha256(await readFile(backup, "utf8")) !== entry.v1Sha256)
|
|
135
|
+
throw new Error(`backup of ${entry.name} did not verify; nothing applied`);
|
|
136
|
+
manifest.entries.push({
|
|
137
|
+
name: entry.name,
|
|
138
|
+
v1Sha256: entry.v1Sha256,
|
|
139
|
+
v2Sha256: entry.v2Sha256,
|
|
140
|
+
backup: path.basename(backup),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
await atomicWrite(manifestFile(), `${JSON.stringify(manifest, null, 2)}\n`, 0o600);
|
|
144
|
+
for (const entry of pending)
|
|
145
|
+
await atomicWrite(entry.file, entry.next, 0o600);
|
|
146
|
+
return { applied: pending.map((entry) => entry.name), blocked: [] };
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Restores each v1 copy, but only over a file still exactly as the upgrade wrote it; a
|
|
150
|
+
// profile edited since then is reported and left alone.
|
|
151
|
+
export async function rollbackProfileUpgrade() {
|
|
152
|
+
return withFileLock(manifestFile(), async () => {
|
|
153
|
+
const manifest = await readManifest();
|
|
154
|
+
if (!manifest)
|
|
155
|
+
throw new Error("No profile upgrade to roll back");
|
|
156
|
+
const restored = [];
|
|
157
|
+
const skipped = [];
|
|
158
|
+
for (const entry of manifest.entries) {
|
|
159
|
+
const file = path.join(profilesDir(), `${entry.name}.json`);
|
|
160
|
+
const current = await readFile(file, "utf8").catch(() => null);
|
|
161
|
+
if (current === null || sha256(current) === entry.v1Sha256) {
|
|
162
|
+
if (current !== null)
|
|
163
|
+
restored.push(entry.name);
|
|
164
|
+
else
|
|
165
|
+
skipped.push({ name: entry.name, reason: "profile file is missing" });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (sha256(current) !== entry.v2Sha256) {
|
|
169
|
+
skipped.push({ name: entry.name, reason: "edited since the upgrade" });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const backup = await readFile(path.join(upgradeDir(), entry.backup), "utf8");
|
|
173
|
+
if (sha256(backup) !== entry.v1Sha256) {
|
|
174
|
+
skipped.push({ name: entry.name, reason: "backup does not verify" });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
await atomicWrite(file, backup, 0o600);
|
|
178
|
+
restored.push(entry.name);
|
|
179
|
+
}
|
|
180
|
+
return { restored, skipped };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Loads every profile the way a run would and reports each failure by name. Messages are
|
|
184
|
+
// validation errors, which name fields, never values from a credential check.
|
|
185
|
+
export async function validateProfiles(load) {
|
|
186
|
+
const dir = profilesDir();
|
|
187
|
+
const names = new Set();
|
|
188
|
+
for (const entry of await readdir(dir).catch(() => [])) {
|
|
189
|
+
if (entry.startsWith("."))
|
|
190
|
+
continue;
|
|
191
|
+
if (entry.endsWith(".json"))
|
|
192
|
+
names.add(entry.slice(0, -".json".length));
|
|
193
|
+
else if (await isDirectory(path.join(dir, entry)))
|
|
194
|
+
names.add(entry);
|
|
195
|
+
}
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const name of [...names].sort()) {
|
|
198
|
+
try {
|
|
199
|
+
const profile = await load(name);
|
|
200
|
+
results.push({ name, ok: true, schemaVersion: profile.schemaVersion });
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
results.push({ name, ok: false, error: error.message });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=profile-upgrade-service.js.map
|