dev-flow-deepseek 0.1.0 → 0.5.1

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 CHANGED
@@ -1,129 +1,44 @@
1
1
  # dev-flow-deepseek
2
2
 
3
- `dev-flow-deepseek` is a prebuilt DeepSeek Harness bundle that connects
4
- the official DSH MCP client to an independently installed Dev Flow Orchestrator
5
- runtime. Its local STDIO projection proxy exposes exactly six tools and renders
6
- each complete Dev Flow result envelope as deterministic canonical JSON while
7
- preserving valid upstream success and domain-error semantics. The external
8
- Python Controller remains the only workflow and task-state authority.
3
+ `dev-flow-deepseek` is the independently versioned DeepSeek Harness adapter to Dev Flow's current
4
+ process graph. Its package metadata is public-release capable; actual support begins only after the
5
+ selected release command and registry lifecycle gate complete.
9
6
 
10
- ## Exact compatibility
7
+ Its product version comes only from `package.json`. The packaged Core reports its own independently
8
+ maintained version; the adapter validates the actual executable, six-tool catalog, live schemas,
9
+ server information, process definition, and behavior without comparing the two product versions.
11
10
 
12
- | Component | Supported baseline |
13
- |---|---|
14
- | This package | `0.1.0` |
15
- | DeepSeek Harness | `@deepseek-ai/dsh@0.1.0-rc.6`, npm SHA-1 `de9fbf39056c7f4e658a3e284cb1d66ebc86d040`, public source baseline `47f943859bef60e4160492346772ded9b24f765a` |
16
- | Dev Flow Orchestrator | release `0.6.12`, commit `7edd4fa12fe6787aac57f16246cbab00b8d79272` |
17
- | Dev Flow MCP interface | `dev-flow-mcp/1.0.0` |
18
- | Dev Flow result schema | `dev-flow-mcp-result/1.0.0` |
19
- | Dev Flow model namespace | `0.4.0` |
20
- | Node.js | `^22.19.0 || >=24.0.0` |
21
- | MCP SDK | `1.12.0` |
11
+ The package targets macOS arm64 with Node.js 24 or newer and declares compatibility with the DSH
12
+ `>=0.1.0-rc.6` service family. Compatibility uses this minimum version without an upper bound;
13
+ release evidence records the actual DSH version used by the final registry lifecycle.
22
14
 
23
- Compatibility is exact. Later DSH or Dev Flow releases are not inferred to be
24
- compatible.
15
+ ## Profile lifecycle
25
16
 
26
- ## Prerequisites and installation
17
+ DSH owns installation and profile reconciliation. Add a reviewed local artifact with:
27
18
 
28
- Install DeepSeek Harness `0.1.0-rc.6` and the separate `dev-flow-mcp` executable
29
- from Dev Flow Orchestrator `0.6.12` before using this bundle. The executable may
30
- be on `PATH` or selected with an absolute `DEV_FLOW_MCP_COMMAND` value. This npm
31
- package does not install, update, repair, migrate, or remove Python, `uv`, or the
32
- Dev Flow runtime.
33
-
34
- Install the stable release into an isolated DSH profile:
35
-
36
- ```sh
37
- dsh plugin --profile web add dev-flow-deepseek@latest
38
- ```
39
-
40
- Alternatively, build and pack locally with the pinned package manager:
41
-
42
- ```sh
43
- corepack pnpm@11.7.0 install --frozen-lockfile
44
- corepack pnpm@11.7.0 run build
45
- corepack pnpm@11.7.0 pack
19
+ ```bash
20
+ dsh plugin --profile <profile> add /absolute/path/to/dev-flow-deepseek-<version>.tgz
46
21
  ```
47
22
 
48
- Add the resulting tarball to an isolated DSH profile, for example the `web`
49
- profile, then remove it by package name when no longer needed:
23
+ Stop and restart that DSH profile before validating the resolved bundle. Remove the package with:
50
24
 
51
- ```sh
52
- dsh plugin --profile web add /absolute/path/dev-flow-deepseek-0.1.0.tgz
53
- dsh plugin --profile web remove dev-flow-deepseek
25
+ ```bash
26
+ dsh plugin --profile <profile> remove dev-flow-deepseek
54
27
  ```
55
28
 
56
- Removal deletes only the profile dependency and bundle layer. It does not
57
- delete Dev Flow task data.
58
-
59
- ## Data and use
60
-
61
- By default the bundle sets `DEV_FLOW_DATA_DIR` to
62
- `$DSH_HOME/dev-flow-orchestrator/data`; the compatible backend appends its
63
- `0.4.0` namespace. This keeps DSH tasks separate from Codex Dev Flow data. An
64
- explicit `DEV_FLOW_DATA_DIR` set before DSH starts overrides that root.
65
-
66
- Inside one existing Git worktree, invoke the Skill explicitly:
67
-
68
- ```text
69
- /dev-flow Implement the requested change and verify it.
70
- ```
71
-
72
- The Skill validates the exact runtime identity, discovers a compatible active
73
- task or starts one `lite` task, follows only the Controller's fresh action and
74
- binding, and stops on the Controller's Delivery Dossier.
75
-
76
- The outward raw MCP surface is exactly:
77
-
78
- - `dev_flow_server_info`
79
- - `dev_flow_find_tasks_for_path`
80
- - `dev_flow_get_task`
81
- - `dev_flow_get_next_action`
82
- - `dev_flow_start_task`
83
- - `dev_flow_apply_action`
84
-
85
- ## Supported and unsupported scope
86
-
87
- The MVP supports explicit `/dev-flow`, one current Git repository, compatible
88
- active `lite` task resume, new `lite` tasks, local STDIO, safe mutation
89
- read-after-write recovery, and complete spilled-result retrieval.
90
-
91
- It does not support implicit activation, multiple repositories, another
92
- workflow, task listing or cancellation, governance decisions, finding
93
- disposition, contract revision, custom DSH UI, runtime installation, a generic
94
- shell MCP, HTTP/SSE/remote MCP, OAuth, branch or worktree management, commits,
95
- pushes, pull requests, tags, or releases. Windows is unverified.
96
-
97
- ## Troubleshooting
29
+ Restart after removal before checking that the bundle contributions are absent. Reinstallation uses
30
+ the same official add command and the exact previously reviewed artifact.
98
31
 
99
- - If no Dev Flow tools appear, confirm that the package layer is installed and
100
- that `DEV_FLOW_MCP_COMMAND` resolves directly to the `0.6.12`
101
- `dev-flow-mcp` executable. DSH remains usable when startup fails because the
102
- bundle sets `failOnStartupError: false`.
103
- - If `/dev-flow` reports an incompatible identity, use the exact versions in
104
- the compatibility table. Do not substitute a newer release.
105
- - If a mutation response is lost, do not replay it. The Skill rediscovers a
106
- possibly created task or reads the task and fresh action before any retry.
107
- - If DSH reports a spilled, truncated, pruned, or preview result, the Skill
108
- must read the complete saved canonical JSON text before using authority
109
- fields.
32
+ The official lifecycle owns the DSH dependency entry, bundle layer, integration process, Skill,
33
+ guard, and MCP child. Dev Flow task data, target repositories, and Codex-owned package or
34
+ configuration state remain outside package removal.
110
35
 
111
- ## Trust and verification
36
+ ## Support boundary
112
37
 
113
- The installed bundle executes its prebuilt Node proxy and the selected local
114
- Dev Flow executable outside the DSH agent sandbox. Install the package only
115
- from a publisher you trust. The proxy reduces ambient authority by using no
116
- shell, passing only a closed child environment, opening no listener, and
117
- exposing no tool outside the six-tool allowlist; it does not claim code signing,
118
- provenance attestation, automatic updates, or protection from a compromised
119
- publisher or runtime.
38
+ The package contains one `dsh.bundle.patch` layer and is designed to contribute one integration row.
39
+ Its final Feature 010 artifact will contain one user-invocable `dev-flow` Skill, a current-turn
40
+ selector guard, the official local STDIO MCP client, and one packaged darwin-arm64 Core.
120
41
 
121
- Deterministic verification consists of 25 bounded unit cases, one local fake
122
- MCP suite with exactly eight scenarios, one package-contract check, strict
123
- OpenSpec validation, and the documented default gate. The real-runtime smoke is
124
- opt-in and requires Dev Flow `0.6.12`. A fake backend or static inspection is not
125
- evidence of a real DSH GUI journey. The recorded evidence verifies the real DSH
126
- happy path and restart/resume/removal journey on macOS arm64. The stable
127
- release artifact was separately revalidated for package installation, composed
128
- path resolution, and removal. Linux x64 and Windows remain `UNVERIFIED`; see
129
- `tests/manual/deepseek-harness-journey.md` for the exact evidence boundary.
42
+ Source capability alone is not a public support claim. npm, `deepseek-v*`, GitHub Release assets and
43
+ the registry-installed DSH lifecycle must all be verified by `pnpm run release:deepseek` before the
44
+ support matrix is promoted.
package/cordis.patch.yml CHANGED
@@ -1,27 +1,3 @@
1
1
  - insert:
2
- - id: dev-flow-mcp
3
- name: '@deepseek-ai/dsh-mcp-client'
4
- config:
5
- serverName: dev-flow
6
- transport: stdio
7
- command: !!js process.execPath
8
- args:
9
- - !!js process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('dev-flow-deepseek/lib/proxy.js')
10
- env:
11
- DEV_FLOW_MCP_COMMAND: !!js process.env.DEV_FLOW_MCP_COMMAND ?? 'dev-flow-mcp'
12
- DEV_FLOW_DATA_DIR: !!js process.env.DEV_FLOW_DATA_DIR ?? dshHomePath('dev-flow-orchestrator/data')
13
- toolCallTimeoutMs: 120000
14
- failOnStartupError: false
15
- reconnect:
16
- enabled: true
17
- initialDelayMs: 500
18
- maxDelayMs: 30000
19
- maxAttempts: 10
20
-
21
- - id: dev-flow-skill-provider
22
- name: '@deepseek-ai/dsh-skill-filesystem'
23
- config:
24
- providerName: dev-flow-bundle
25
- includeDefaultRoots: false
26
- customSkillDirs:
27
- - !!js process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('dev-flow-deepseek/package.json')), 'skills')
2
+ - id: dev-flow-deepseek
3
+ name: dev-flow-deepseek
@@ -0,0 +1,117 @@
1
+ import {
2
+ isDevFlowNamespaceTool,
3
+ isExpectedDevFlowTool,
4
+ } from "./tool-names.mjs";
5
+
6
+ export const DENIAL_CODES = Object.freeze({
7
+ SELECTOR_REQUIRED: "DEV_FLOW_SELECTOR_REQUIRED",
8
+ UNEXPECTED_TOOL: "DEV_FLOW_UNEXPECTED_TOOL",
9
+ NO_AGENT: "DEV_FLOW_NO_AGENT",
10
+ NO_OPEN_TURN: "DEV_FLOW_NO_OPEN_TURN",
11
+ });
12
+
13
+ const selectorPattern = /(^|\s)\/dev-flow(?=\s|$)/u;
14
+ const selectorInstruction = "include a whitespace-bounded /dev-flow in the current direct user turn";
15
+
16
+ export function hasDirectUserSelector(message) {
17
+ if (message?.source?.kind !== "user" || !Array.isArray(message.content)) return false;
18
+ return message.content.some(
19
+ (block) => block?.type === "text"
20
+ && typeof block.text === "string"
21
+ && selectorPattern.test(block.text),
22
+ );
23
+ }
24
+
25
+ export function deriveCurrentTurn(execution) {
26
+ try {
27
+ const agent = execution?.agent;
28
+ if (agent?.status !== "running" || !Array.isArray(agent.session?.events)) return undefined;
29
+ const events = agent.session.events;
30
+ const matchingCalls = events
31
+ .map((event, index) => ({ event, index }))
32
+ .filter(({ event }) => event?.type === "tool/call"
33
+ && String(event.data?.callId) === String(execution.callId));
34
+
35
+ if (matchingCalls.length === 1) {
36
+ return deriveDurableCallTurn(execution, events, matchingCalls[0]);
37
+ }
38
+ if (matchingCalls.length > 1 || execution.parent === undefined) return undefined;
39
+ return deriveNestedTurn(events);
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ }
44
+
45
+ export function authorizeDevFlowExecution(execution) {
46
+ if (!isDevFlowNamespaceTool(execution?.name)) return undefined;
47
+ if (!isExpectedDevFlowTool(execution.name)) {
48
+ return `${DENIAL_CODES.UNEXPECTED_TOOL}: the Dev Flow namespace permits only the six contracted tools.`;
49
+ }
50
+ if (execution.agent === undefined) {
51
+ return `${DENIAL_CODES.NO_AGENT}: ${selectorInstruction}.`;
52
+ }
53
+
54
+ const turn = deriveCurrentTurn(execution);
55
+ if (turn === undefined) {
56
+ return `${DENIAL_CODES.NO_OPEN_TURN}: ${selectorInstruction}.`;
57
+ }
58
+ if (!turn.selectorPresent) {
59
+ return `${DENIAL_CODES.SELECTOR_REQUIRED}: ${selectorInstruction}.`;
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ export function registerDevFlowGuard(ctx) {
65
+ return ctx.tools.guard(authorizeDevFlowExecution);
66
+ }
67
+
68
+ function deriveDurableCallTurn(execution, events, matchingCall) {
69
+ const { event: callEvent, index: callIndex } = matchingCall;
70
+ if (callEvent.data?.name !== execution.name) return undefined;
71
+ const turn = callEvent.data?.turn;
72
+ if (!Number.isInteger(turn)) return undefined;
73
+
74
+ const starts = events
75
+ .slice(0, callIndex + 1)
76
+ .map((event, index) => ({ event, index }))
77
+ .filter(({ event }) => event?.type === "turn/start" && event.data?.turn === turn);
78
+ if (starts.length !== 1) return undefined;
79
+ const start = starts[0];
80
+
81
+ for (let index = start.index + 1; index < events.length; index += 1) {
82
+ const event = events[index];
83
+ if (event?.type === "turn/start") return undefined;
84
+ if (event?.type === "turn/end" && event.data?.turn === turn) return undefined;
85
+ }
86
+ return projectTurn(events, start, callIndex, turn, callEvent.seq);
87
+ }
88
+
89
+ function deriveNestedTurn(events) {
90
+ let open;
91
+ for (let index = 0; index < events.length; index += 1) {
92
+ const event = events[index];
93
+ if (event?.type === "turn/start") {
94
+ if (open !== undefined || !Number.isInteger(event.data?.turn)) return undefined;
95
+ open = { event, index };
96
+ } else if (event?.type === "turn/end") {
97
+ if (open === undefined || event.data?.turn !== open.event.data?.turn) return undefined;
98
+ open = undefined;
99
+ }
100
+ }
101
+ if (open === undefined) return undefined;
102
+ return projectTurn(events, open, events.length, open.event.data.turn, undefined);
103
+ }
104
+
105
+ function projectTurn(events, start, endIndex, turn, callSeq) {
106
+ const messages = events
107
+ .slice(start.index + 1, endIndex)
108
+ .filter((event) => event?.type === "user/message" && event.data?.source?.kind === "user")
109
+ .map((event) => event.data);
110
+ return Object.freeze({
111
+ turn,
112
+ startSeq: start.event.seq,
113
+ ...(callSeq === undefined ? {} : { callSeq }),
114
+ directUserMessageIds: Object.freeze(messages.map((message) => message.id)),
115
+ selectorPresent: messages.some(hasDirectUserSelector),
116
+ });
117
+ }
package/lib/index.mjs ADDED
@@ -0,0 +1,114 @@
1
+ import * as mcpClientPlugin from "@deepseek-ai/dsh-mcp-client";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ import { registerDevFlowGuard } from "./authorization.mjs";
6
+ import {
7
+ ensureDefaultDataDirectory,
8
+ packageRootFromModule,
9
+ resolveDataDirectory,
10
+ } from "./paths.mjs";
11
+ import { preflightPackagedCore, selectPackagedRuntime } from "./runtime.mjs";
12
+ import {
13
+ DEV_FLOW_SERVER_NAME,
14
+ assertQualifiedToolCatalog,
15
+ } from "./tool-names.mjs";
16
+
17
+ export const name = "dev-flow-deepseek";
18
+ export const inject = ["skills", "tools"];
19
+
20
+ export async function apply(ctx) {
21
+ await activateDeepSeekIntegration(ctx);
22
+ }
23
+
24
+ export async function activateDeepSeekIntegration(ctx, {
25
+ packageRoot = packageRootFromModule(import.meta.url),
26
+ homeDirectory,
27
+ environment = process.env,
28
+ platform = process.platform,
29
+ arch = process.arch,
30
+ } = {}) {
31
+ const manifest = await readPackageManifest(packageRoot);
32
+ const runtimeSelection = await selectPackagedRuntime({ packageRoot, platform, arch });
33
+ const dataSelection = await resolveDataDirectory({ homeDirectory, environment });
34
+ if (dataSelection.usesDefaultDataDirectory) {
35
+ await ensureDefaultDataDirectory(dataSelection);
36
+ }
37
+ const runtime = await preflightPackagedCore(runtimeSelection, {
38
+ environment,
39
+ });
40
+
41
+ const skillDirectory = join(runtime.packageRoot, "skills", "dev-flow");
42
+ const skillPath = join(skillDirectory, "SKILL.md");
43
+ const skillContent = await readFile(skillPath, "utf8");
44
+
45
+ ctx.skills.register(Object.freeze({
46
+ name: "dev-flow",
47
+ description: "Use the Dev Flow graph Core for the current explicit development request.",
48
+ whenToUse: "Use only for a current direct user turn containing /dev-flow.",
49
+ invocation: Object.freeze({
50
+ modelInvocable: false,
51
+ userInvocable: true,
52
+ }),
53
+ source: "bundled",
54
+ provider: name,
55
+ resourceBase: Object.freeze({ kind: "directory", path: skillDirectory }),
56
+ content: skillContent,
57
+ path: skillPath,
58
+ }));
59
+ registerDevFlowGuard(ctx);
60
+
61
+ let mcpFiber;
62
+ let catalogCheckQueued = false;
63
+ let catalogFailed = false;
64
+ ctx.on("tools/change", () => {
65
+ if (catalogCheckQueued || catalogFailed) return;
66
+ catalogCheckQueued = true;
67
+ queueMicrotask(() => {
68
+ catalogCheckQueued = false;
69
+ if (catalogFailed) return;
70
+ try {
71
+ verifyCurrentCatalog(ctx, { allowUnavailable: true });
72
+ } catch (error) {
73
+ catalogFailed = true;
74
+ ctx.logger.error(`${name}: ${error.message}`);
75
+ if (mcpFiber !== undefined) {
76
+ void mcpFiber.dispose().catch((disposeError) => {
77
+ ctx.logger.error(`${name}: failed to dispose incompatible MCP child: ${disposeError.message}`);
78
+ });
79
+ }
80
+ }
81
+ });
82
+ });
83
+
84
+ mcpFiber = await ctx.plugin(mcpClientPlugin, {
85
+ transport: "stdio",
86
+ serverName: DEV_FLOW_SERVER_NAME,
87
+ command: runtime.runtimePath,
88
+ args: ["mcp", "--stdio"],
89
+ env: { DEV_FLOW_DATA_DIR: dataSelection.dataDirectory },
90
+ cwd: runtime.packageRoot,
91
+ toolCallTimeoutMs: 60_000,
92
+ failOnStartupError: false,
93
+ reconnect: {
94
+ enabled: true,
95
+ initialDelayMs: 500,
96
+ maxDelayMs: 30_000,
97
+ maxAttempts: 10,
98
+ },
99
+ });
100
+ verifyCurrentCatalog(ctx, { allowUnavailable: true });
101
+ }
102
+
103
+ function verifyCurrentCatalog(ctx, options) {
104
+ const toolNames = ctx.tools.schemas().map((schema) => schema.name);
105
+ return assertQualifiedToolCatalog(toolNames, options);
106
+ }
107
+
108
+ async function readPackageManifest(packageRoot) {
109
+ const manifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
110
+ if (manifest.name !== name || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.test(manifest.version ?? "")) {
111
+ throw new Error("DeepSeek package manifest identity is invalid");
112
+ }
113
+ return manifest;
114
+ }
package/lib/paths.mjs ADDED
@@ -0,0 +1,121 @@
1
+ import { chmod, lstat, mkdir, realpath, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const DATA_DIRECTORY_ENVIRONMENT = "DEV_FLOW_DATA_DIR";
7
+
8
+ export function packageRootFromModule(moduleUrl = import.meta.url) {
9
+ return dirname(dirname(fileURLToPath(moduleUrl)));
10
+ }
11
+
12
+ export async function resolveDataDirectory({
13
+ homeDirectory = homedir(),
14
+ environment = process.env,
15
+ } = {}) {
16
+ const canonicalHome = await canonicalExistingDirectory(homeDirectory, "home directory");
17
+ const productSupportRoot = containedPath(
18
+ canonicalHome,
19
+ join(canonicalHome, "Library", "Application Support", "dev-flow"),
20
+ "product support root",
21
+ );
22
+
23
+ const explicitDataDirectory = environment?.[DATA_DIRECTORY_ENVIRONMENT] ?? "";
24
+ if (explicitDataDirectory !== "") {
25
+ const dataDirectory = await canonicalExplicitDataDirectory(explicitDataDirectory);
26
+ return Object.freeze({
27
+ dataDirectory,
28
+ homeDirectory: canonicalHome,
29
+ productSupportRoot,
30
+ usesDefaultDataDirectory: false,
31
+ });
32
+ }
33
+
34
+ await assertNoSymlinkComponents(canonicalHome, productSupportRoot);
35
+
36
+ return Object.freeze({
37
+ dataDirectory: containedPath(
38
+ productSupportRoot,
39
+ join(productSupportRoot, "data"),
40
+ "default data directory",
41
+ ),
42
+ homeDirectory: canonicalHome,
43
+ productSupportRoot,
44
+ usesDefaultDataDirectory: true,
45
+ });
46
+ }
47
+
48
+ export async function ensureDefaultDataDirectory(paths) {
49
+ if (!paths?.usesDefaultDataDirectory) {
50
+ throw new Error("refusing to create an explicit data directory");
51
+ }
52
+ const expected = containedPath(
53
+ paths.productSupportRoot,
54
+ join(paths.productSupportRoot, "data"),
55
+ "default data directory",
56
+ );
57
+ if (paths.dataDirectory !== expected) {
58
+ throw new Error("default data directory does not match the shared product path");
59
+ }
60
+
61
+ await assertNoSymlinkComponents(paths.homeDirectory, paths.productSupportRoot);
62
+ await mkdir(paths.dataDirectory, { recursive: true, mode: 0o700 });
63
+ await assertNoSymlinkComponents(paths.homeDirectory, paths.dataDirectory);
64
+ await chmod(paths.dataDirectory, 0o700);
65
+ return paths.dataDirectory;
66
+ }
67
+
68
+ export function containedPath(root, candidate, label = "path") {
69
+ const canonicalRoot = resolve(root);
70
+ const canonicalCandidate = resolve(candidate);
71
+ const offset = relative(canonicalRoot, canonicalCandidate);
72
+ if (offset === ".." || offset.startsWith(`..${sep}`) || isAbsolute(offset)) {
73
+ throw new Error(`${label} escapes its owned root`);
74
+ }
75
+ return canonicalCandidate;
76
+ }
77
+
78
+ async function canonicalExplicitDataDirectory(path) {
79
+ if (!isAbsolute(path)) {
80
+ throw new Error(`${DATA_DIRECTORY_ENVIRONMENT} must be an absolute path`);
81
+ }
82
+ const normalized = resolve(path);
83
+ const canonical = await canonicalExistingDirectory(path, DATA_DIRECTORY_ENVIRONMENT);
84
+ if (canonical !== normalized) {
85
+ throw new Error(`${DATA_DIRECTORY_ENVIRONMENT} must be canonical and may not use a symbolic link`);
86
+ }
87
+ return canonical;
88
+ }
89
+
90
+ async function canonicalExistingDirectory(path, label) {
91
+ if (!isAbsolute(path)) {
92
+ throw new Error(`${label} must be absolute`);
93
+ }
94
+ try {
95
+ const canonical = await realpath(path);
96
+ const info = await stat(canonical);
97
+ if (!info.isDirectory()) throw new Error("not a directory");
98
+ return canonical;
99
+ } catch (error) {
100
+ throw new Error(`${label} must name an existing directory`, { cause: error });
101
+ }
102
+ }
103
+
104
+ async function assertNoSymlinkComponents(root, candidate) {
105
+ const canonicalRoot = resolve(root);
106
+ const canonicalCandidate = containedPath(canonicalRoot, candidate, "product path");
107
+ const components = relative(canonicalRoot, canonicalCandidate).split(sep).filter(Boolean);
108
+ let current = canonicalRoot;
109
+ for (const component of components) {
110
+ current = join(current, component);
111
+ try {
112
+ const info = await lstat(current);
113
+ if (info.isSymbolicLink()) {
114
+ throw new Error(`product path contains a symbolic link: ${current}`);
115
+ }
116
+ } catch (error) {
117
+ if (error?.code === "ENOENT") return;
118
+ throw error;
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,103 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { lstat, realpath, stat } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ import { containedPath, packageRootFromModule } from "./paths.mjs";
7
+
8
+ export const SUPPORTED_RUNTIME_KEY = "darwin-arm64";
9
+
10
+ const execFile = promisify(execFileCallback);
11
+ const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
12
+
13
+ export async function selectPackagedRuntime({
14
+ packageRoot = packageRootFromModule(import.meta.url),
15
+ platform = process.platform,
16
+ arch = process.arch,
17
+ } = {}) {
18
+ const runtimeKey = `${platform}-${arch}`;
19
+ if (runtimeKey !== SUPPORTED_RUNTIME_KEY) {
20
+ throw new Error(`unsupported platform ${runtimeKey}; dev-flow-deepseek supports ${SUPPORTED_RUNTIME_KEY}`);
21
+ }
22
+
23
+ const canonicalPackageRoot = await canonicalPackageDirectory(packageRoot);
24
+ return Object.freeze({
25
+ packageRoot: canonicalPackageRoot,
26
+ runtimeKey,
27
+ runtimePath: containedPath(
28
+ canonicalPackageRoot,
29
+ join(canonicalPackageRoot, "runtime", runtimeKey, "dev-flow"),
30
+ "packaged Core runtime",
31
+ ),
32
+ });
33
+ }
34
+
35
+ export async function preflightPackagedCore(
36
+ selection,
37
+ {
38
+ environment = process.env,
39
+ currentDirectory = dirname(selection?.runtimePath ?? "."),
40
+ } = {},
41
+ ) {
42
+ if (!selection || selection.runtimeKey !== SUPPORTED_RUNTIME_KEY) {
43
+ throw new Error("packaged Core selection must use darwin-arm64");
44
+ }
45
+ const expectedRuntimePath = containedPath(
46
+ selection.packageRoot,
47
+ join(selection.packageRoot, "runtime", SUPPORTED_RUNTIME_KEY, "dev-flow"),
48
+ "packaged Core runtime",
49
+ );
50
+ if (selection.runtimePath !== expectedRuntimePath) {
51
+ throw new Error("packaged Core must use the exact package-relative runtime path");
52
+ }
53
+ await assertRegularExecutableFile(selection.runtimePath);
54
+
55
+ let stdout;
56
+ try {
57
+ ({ stdout } = await execFile(selection.runtimePath, ["version"], {
58
+ cwd: currentDirectory,
59
+ env: environment,
60
+ encoding: "utf8",
61
+ maxBuffer: 64 * 1024,
62
+ windowsHide: true,
63
+ }));
64
+ } catch (error) {
65
+ throw new Error("packaged Core version preflight failed", { cause: error });
66
+ }
67
+
68
+ const match = /^dev-flow (\S+)\n?$/u.exec(stdout);
69
+ if (!match || !semverPattern.test(match[1])) {
70
+ throw new Error("packaged Core returned an invalid version line");
71
+ }
72
+ return Object.freeze({ ...selection, version: match[1] });
73
+ }
74
+
75
+ async function canonicalPackageDirectory(path) {
76
+ try {
77
+ const canonical = await realpath(path);
78
+ const info = await stat(canonical);
79
+ if (!info.isDirectory()) throw new Error("not a directory");
80
+ return canonical;
81
+ } catch (error) {
82
+ throw new Error("package root must name an existing directory", { cause: error });
83
+ }
84
+ }
85
+
86
+ async function assertRegularExecutableFile(path) {
87
+ let info;
88
+ try {
89
+ info = await lstat(path);
90
+ } catch (error) {
91
+ throw new Error("packaged Core must be a regular executable file", { cause: error });
92
+ }
93
+ if (!info.isFile() || info.isSymbolicLink()) {
94
+ throw new Error("packaged Core must be a regular executable file");
95
+ }
96
+ if ((info.mode & 0o111) === 0) {
97
+ throw new Error("packaged Core must have executable mode");
98
+ }
99
+ const canonical = await realpath(path);
100
+ if (canonical !== path) {
101
+ throw new Error("packaged Core must use a canonical package-relative runtime path");
102
+ }
103
+ }