@metagptx/deepflow-suite-cli 0.0.0-stage → 0.1.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.
Files changed (36) hide show
  1. package/README.md +48 -2
  2. package/bin/deepflow-suite.js +77 -0
  3. package/bundle/deepflow-release.json +185 -0
  4. package/bundle/skills/deepflow-suite/SKILL.md +79 -0
  5. package/bundle/skills/deepflow-suite/manifest.yaml +20 -0
  6. package/bundle/skills/df-agentctrl/SKILL.md +143 -0
  7. package/bundle/skills/df-agentctrl/assets/agent.json +8 -0
  8. package/bundle/skills/df-agentctrl/references/maintenance.md +68 -0
  9. package/bundle/skills/df-agentctrl/scripts/to_snapshot.py +26 -0
  10. package/bundle/skills/df-agentrt/SKILL.md +191 -0
  11. package/bundle/skills/df-agentrt-repo/SKILL.md +83 -0
  12. package/bundle/skills/df-task/SKILL.md +177 -0
  13. package/bundle/skills/df-task/assets/payload-examples/adjudication.json +5 -0
  14. package/bundle/skills/df-task/assets/payload-examples/artifact_published.json +6 -0
  15. package/bundle/skills/df-task/assets/payload-examples/help_requested.json +5 -0
  16. package/bundle/skills/df-task/assets/payload-examples/run_outcome.json +5 -0
  17. package/bundle/skills/df-task/assets/payload-examples/run_terminated.json +4 -0
  18. package/bundle/skills/df-task/assets/payload-examples/summary.json +4 -0
  19. package/bundle/skills/df-task/references/fact-types.md +48 -0
  20. package/bundle/skills/df-task/skill_test.go +201 -0
  21. package/bundle/skills/df-taskdriver/SKILL.md +126 -0
  22. package/bundle/skills/df-workbench/SKILL.md +64 -0
  23. package/bundle/skills/df-workbench/manifest.yaml +5 -0
  24. package/manifest.json +68 -0
  25. package/package.json +28 -4
  26. package/scripts/build-package.mjs +12 -0
  27. package/skills/deepflow-suite/SKILL.md +79 -0
  28. package/skills/deepflow-suite/manifest.yaml +20 -0
  29. package/src/bundle-installer.mjs +189 -0
  30. package/src/bundle.mjs +128 -0
  31. package/src/cli-install.mjs +140 -0
  32. package/src/commands/doctor.mjs +35 -0
  33. package/src/commands/install.mjs +32 -0
  34. package/src/constants.mjs +30 -0
  35. package/src/linker.mjs +96 -0
  36. package/src/manifest.mjs +72 -0
@@ -0,0 +1,189 @@
1
+ import { access, cp, lstat, mkdir, mkdtemp, readdir, readFile, rename, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { CLI_NAME, DISTRIBUTION, PACKAGE_NAME, RELEASE_FILE, SKILL_NAMES } from "./constants.mjs";
5
+ import { listFiles } from "./bundle.mjs";
6
+
7
+ async function exists(filePath) {
8
+ try {
9
+ await access(filePath);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ async function assertRegularTree(root) {
17
+ const rootStats = await lstat(root);
18
+ if (rootStats.isSymbolicLink()) throw new Error(`bundle contains a symbolic link: ${root}`);
19
+ if (!rootStats.isDirectory()) throw new Error(`bundle path is not a directory: ${root}`);
20
+ const entries = await readdir(root, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ const entryPath = path.join(root, entry.name);
23
+ const stats = await lstat(entryPath);
24
+ if (stats.isSymbolicLink()) throw new Error(`bundle contains a symbolic link: ${entryPath}`);
25
+ if (entry.isDirectory()) await assertRegularTree(entryPath);
26
+ else if (!stats.isFile()) throw new Error(`bundle contains a non-regular file: ${entryPath}`);
27
+ }
28
+ }
29
+
30
+ function validateRelease(release, expectedVersion) {
31
+ if (!release || typeof release !== "object") throw new Error("invalid DeepFlow bundle release metadata");
32
+ if (release.schemaVersion !== 1) throw new Error("unsupported DeepFlow bundle schema version");
33
+ if (release.distribution !== DISTRIBUTION) throw new Error(`bundle distribution must be ${DISTRIBUTION}`);
34
+ if (release.package !== PACKAGE_NAME) throw new Error(`bundle package must be ${PACKAGE_NAME}`);
35
+ if (typeof release.version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(release.version)) {
36
+ throw new Error("bundle version must be a semantic version");
37
+ }
38
+ if (expectedVersion && release.version !== expectedVersion) {
39
+ throw new Error(`bundle version ${release.version} does not match ${expectedVersion}`);
40
+ }
41
+ if (!release.cli || release.cli.name !== CLI_NAME || release.cli.package !== PACKAGE_NAME
42
+ || release.cli.version !== release.version || release.cli.command !== CLI_NAME) {
43
+ throw new Error("bundle CLI metadata does not match deepflow-suite");
44
+ }
45
+ if (!["any", process.platform].includes(release.platform)) {
46
+ throw new Error(`bundle platform ${release.platform} does not match ${process.platform}`);
47
+ }
48
+ if (!["any", process.arch].includes(release.arch)) {
49
+ throw new Error(`bundle architecture ${release.arch} does not match ${process.arch}`);
50
+ }
51
+ if (!Array.isArray(release.skills) || release.skills.length !== SKILL_NAMES.length) {
52
+ throw new Error("bundle must contain exactly seven skills, including deepflow-suite");
53
+ }
54
+ const names = release.skills.map((skill) => skill?.name);
55
+ if (new Set(names).size !== names.length || JSON.stringify([...names].sort()) !== JSON.stringify([...SKILL_NAMES].sort())) {
56
+ throw new Error("bundle must contain exactly the manifest skills, including deepflow-suite");
57
+ }
58
+ for (const skill of release.skills) {
59
+ if (!Array.isArray(skill.files) || skill.files.length === 0) {
60
+ throw new Error(`bundle skill ${skill.name} is missing file metadata`);
61
+ }
62
+ for (const file of skill.files) {
63
+ if (!file || typeof file.path !== "string" || path.isAbsolute(file.path) || file.path.split(/[\\/]/).includes("..")) {
64
+ throw new Error(`bundle skill ${skill.name} contains an invalid file path`);
65
+ }
66
+ if (!Number.isSafeInteger(file.size) || file.size < 0 || !/^[a-f0-9]{64}$/.test(file.sha256)) {
67
+ throw new Error(`bundle skill ${skill.name} contains invalid file metadata`);
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ async function verifySkillFiles(bundleDir, skill) {
74
+ const source = path.join(bundleDir, "skills", skill.name);
75
+ await assertRegularTree(source);
76
+ const actual = await listFiles(source);
77
+ const expected = [...skill.files].sort((left, right) => left.path.localeCompare(right.path));
78
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
79
+ throw new Error(`bundle skill ${skill.name} file size or sha256 does not match release metadata`);
80
+ }
81
+ const skillFile = path.join(source, "SKILL.md");
82
+ if (!await exists(skillFile)) throw new Error(`bundle skill ${skill.name} is missing SKILL.md`);
83
+ }
84
+
85
+ async function verifyInstalledSkill(targetDir, skill) {
86
+ const source = path.join(targetDir, skill.name);
87
+ await assertRegularTree(source);
88
+ const actual = await listFiles(source);
89
+ const expected = [...skill.files].sort((left, right) => left.path.localeCompare(right.path));
90
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
91
+ throw new Error(`installed Skill ${skill.name} file size or sha256 does not match release metadata`);
92
+ }
93
+ if (!await exists(path.join(source, "SKILL.md"))) {
94
+ throw new Error(`installed Skill ${skill.name} is missing SKILL.md`);
95
+ }
96
+ }
97
+
98
+ async function inspect(pathname) {
99
+ try {
100
+ return await lstat(pathname);
101
+ } catch (error) {
102
+ if (error.code === "ENOENT") return null;
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ async function installTransaction({ bundleDir, targetDir, names, force }) {
108
+ if (!targetDir) throw new Error("Skill target directory is required");
109
+ const resolvedTarget = path.resolve(targetDir);
110
+ const parent = path.dirname(resolvedTarget);
111
+ await mkdir(parent, { recursive: true });
112
+ const targetStats = await inspect(resolvedTarget);
113
+ if (targetStats && !targetStats.isDirectory()) throw new Error(`Skill target is not a directory: ${resolvedTarget}`);
114
+
115
+ for (const name of names) {
116
+ const target = path.join(resolvedTarget, name);
117
+ const stats = await inspect(target);
118
+ if (stats && !force && !stats.isSymbolicLink()) {
119
+ throw new Error(`refusing to replace existing Skill without --force: ${target}`);
120
+ }
121
+ }
122
+
123
+ const staging = await mkdtemp(path.join(parent, `.${path.basename(resolvedTarget)}.tmp-`));
124
+ try {
125
+ for (const name of names) {
126
+ await cp(path.join(bundleDir, "skills", name), path.join(staging, name), {
127
+ recursive: true,
128
+ dereference: false,
129
+ });
130
+ }
131
+ await mkdir(resolvedTarget, { recursive: true });
132
+ const backup = await mkdtemp(path.join(resolvedTarget, ".deepflow-backup-"));
133
+ const installed = [];
134
+ const backedUp = [];
135
+ try {
136
+ for (const name of names) {
137
+ const target = path.join(resolvedTarget, name);
138
+ if (await inspect(target)) {
139
+ await rename(target, path.join(backup, name));
140
+ backedUp.push(name);
141
+ }
142
+ await rename(path.join(staging, name), target);
143
+ installed.push(name);
144
+ }
145
+ await rm(backup, { recursive: true, force: true });
146
+ return { targetDir: resolvedTarget, installed };
147
+ } catch (error) {
148
+ for (const name of installed) await rm(path.join(resolvedTarget, name), { recursive: true, force: true });
149
+ for (const name of backedUp) await rename(path.join(backup, name), path.join(resolvedTarget, name));
150
+ await rm(backup, { recursive: true, force: true });
151
+ throw error;
152
+ }
153
+ } finally {
154
+ await rm(staging, { recursive: true, force: true });
155
+ }
156
+ }
157
+
158
+ export async function verifyBundle({ bundleDir, expectedVersion }) {
159
+ if (!bundleDir) throw new Error("bundle directory is required");
160
+ const release = JSON.parse(await readFile(path.join(bundleDir, RELEASE_FILE), "utf8"));
161
+ validateRelease(release, expectedVersion);
162
+ const sourceDir = path.join(bundleDir, "skills");
163
+ const entries = await readdir(sourceDir, { withFileTypes: true });
164
+ if (entries.some((entry) => !entry.isDirectory())) {
165
+ throw new Error("bundle skills directory must contain only Skill directories");
166
+ }
167
+ const actual = entries.map((entry) => entry.name).sort();
168
+ if (JSON.stringify(actual) !== JSON.stringify([...SKILL_NAMES].sort())) {
169
+ throw new Error("bundle must contain exactly the manifest skills, including deepflow-suite");
170
+ }
171
+ for (const skill of release.skills) await verifySkillFiles(bundleDir, skill);
172
+ return release;
173
+ }
174
+
175
+ export async function verifyInstalledBundle({ bundleDir, targetDir, expectedVersion }) {
176
+ const release = await verifyBundle({ bundleDir, expectedVersion });
177
+ if (!targetDir) throw new Error("Skill target directory is required");
178
+ const stats = await inspect(path.resolve(targetDir));
179
+ if (!stats || !stats.isDirectory()) throw new Error(`Skill target directory is missing: ${targetDir}`);
180
+ for (const skill of release.skills) await verifyInstalledSkill(path.resolve(targetDir), skill);
181
+ return release;
182
+ }
183
+
184
+ export async function installBundleSkills({ bundleDir, targetDir, force = false, expectedVersion }) {
185
+ await verifyBundle({ bundleDir, expectedVersion });
186
+ return installTransaction({ bundleDir, targetDir, names: SKILL_NAMES, force });
187
+ }
188
+
189
+ export { validateRelease };
package/src/bundle.mjs ADDED
@@ -0,0 +1,128 @@
1
+ import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import path from "node:path";
4
+
5
+ import { CLI_NAME, PACKAGE_NAME, RELEASE_FILE } from "./constants.mjs";
6
+ import { loadManifest, resolveSkillSources } from "./manifest.mjs";
7
+
8
+ async function hashFile(filePath) {
9
+ const contents = await readFile(filePath);
10
+ return {
11
+ size: contents.byteLength,
12
+ sha256: createHash("sha256").update(contents).digest("hex"),
13
+ };
14
+ }
15
+
16
+ async function assertNoSymlinks(root) {
17
+ const rootStats = await lstat(root);
18
+ if (rootStats.isSymbolicLink()) throw new Error(`bundle source contains a symbolic link: ${root}`);
19
+ if (!rootStats.isDirectory()) throw new Error(`bundle source is not a directory: ${root}`);
20
+ const entries = await readdir(root, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ const entryPath = path.join(root, entry.name);
23
+ const stats = await lstat(entryPath);
24
+ if (stats.isSymbolicLink()) {
25
+ throw new Error(`bundle contains a symbolic link: ${entryPath}`);
26
+ }
27
+ if (entry.isDirectory()) await assertNoSymlinks(entryPath);
28
+ }
29
+ }
30
+
31
+ async function listFiles(root, current = root) {
32
+ const entries = await readdir(current, { withFileTypes: true });
33
+ const files = [];
34
+ for (const entry of entries) {
35
+ const absolute = path.join(current, entry.name);
36
+ if (entry.isDirectory()) {
37
+ files.push(...await listFiles(root, absolute));
38
+ continue;
39
+ }
40
+ const stats = await lstat(absolute);
41
+ if (!stats.isFile()) throw new Error(`bundle contains a non-regular file: ${absolute}`);
42
+ const digest = await hashFile(absolute);
43
+ files.push({
44
+ path: path.relative(root, absolute).split(path.sep).join("/"),
45
+ ...digest,
46
+ });
47
+ }
48
+ return files.sort((left, right) => left.path.localeCompare(right.path));
49
+ }
50
+
51
+ async function assembleInto({ workspaceRoot, outputDir, manifestPath }) {
52
+ const manifest = await loadManifest(manifestPath);
53
+ const sources = resolveSkillSources(manifest, workspaceRoot);
54
+ await mkdir(path.join(outputDir, "skills"), { recursive: true });
55
+
56
+ const skills = [];
57
+ for (const skill of sources) {
58
+ await assertNoSymlinks(skill.sourcePath);
59
+ const skillFile = path.join(skill.sourcePath, "SKILL.md");
60
+ const skillStats = await lstat(skillFile).catch((error) => {
61
+ if (error.code === "ENOENT") throw new Error(`missing Skill source: ${skillFile}`);
62
+ throw error;
63
+ });
64
+ if (!skillStats.isFile() || skillStats.size === 0) {
65
+ throw new Error(`missing or empty Skill source: ${skillFile}`);
66
+ }
67
+ const destination = path.join(outputDir, "skills", skill.name);
68
+ await cp(skill.sourcePath, destination, { recursive: true, dereference: false });
69
+ skills.push({
70
+ name: skill.name,
71
+ source: skill.source,
72
+ files: await listFiles(destination),
73
+ });
74
+ }
75
+
76
+ const release = {
77
+ schemaVersion: 1,
78
+ distribution: manifest.distribution,
79
+ package: PACKAGE_NAME,
80
+ version: manifest.version,
81
+ platform: process.platform,
82
+ arch: process.arch,
83
+ cli: {
84
+ name: CLI_NAME,
85
+ package: PACKAGE_NAME,
86
+ version: manifest.version,
87
+ command: CLI_NAME,
88
+ },
89
+ skills,
90
+ clis: manifest.clis,
91
+ };
92
+ await writeFile(
93
+ path.join(outputDir, RELEASE_FILE),
94
+ `${JSON.stringify(release, null, 2)}\n`,
95
+ );
96
+ return { skills: skills.map((skill) => skill.name), manifest: release };
97
+ }
98
+
99
+ export async function assembleBundle({ workspaceRoot, outputDir, manifestPath }) {
100
+ if (!outputDir) throw new Error("bundle output directory is required");
101
+ const resolvedOutput = path.resolve(outputDir);
102
+ await mkdir(path.dirname(resolvedOutput), { recursive: true });
103
+ const staging = await mkdtemp(path.join(path.dirname(resolvedOutput), `.${path.basename(resolvedOutput)}.tmp-`));
104
+ try {
105
+ const result = await assembleInto({ workspaceRoot, outputDir: staging, manifestPath });
106
+ const backup = `${resolvedOutput}.backup-${process.pid}-${Date.now()}`;
107
+ let movedExisting = false;
108
+ try {
109
+ await rename(resolvedOutput, backup);
110
+ movedExisting = true;
111
+ } catch (error) {
112
+ if (error.code !== "ENOENT") throw error;
113
+ }
114
+ try {
115
+ await rename(staging, resolvedOutput);
116
+ } catch (error) {
117
+ if (movedExisting) await rename(backup, resolvedOutput);
118
+ throw error;
119
+ }
120
+ if (movedExisting) await rm(backup, { recursive: true, force: true });
121
+ return { ...result, outputDir: resolvedOutput };
122
+ } catch (error) {
123
+ await rm(staging, { recursive: true, force: true });
124
+ throw error;
125
+ }
126
+ }
127
+
128
+ export { listFiles };
@@ -0,0 +1,140 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access, lstat, mkdir, mkdtemp, readlink, rename, rm, symlink, unlink } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ function run(command, args, options) {
7
+ return new Promise((resolve, reject) => {
8
+ const child = spawn(command, args, { cwd: options.cwd, stdio: "inherit", env: options.env });
9
+ child.once("error", reject);
10
+ child.once("exit", (code, signal) => {
11
+ if (code === 0) resolve();
12
+ else reject(new Error(`${command} ${args.join(" ")} exited with ${code ?? signal}`));
13
+ });
14
+ });
15
+ }
16
+
17
+ async function replaceLink(source, target) {
18
+ await mkdir(path.dirname(target), { recursive: true });
19
+ try {
20
+ const stats = await lstat(target);
21
+ if (!stats.isSymbolicLink()) {
22
+ throw new Error(`refusing to replace non-symlink CLI target: ${target}`);
23
+ }
24
+ const current = path.resolve(path.dirname(target), await readlink(target));
25
+ if (current === source) return "unchanged";
26
+ await unlink(target);
27
+ } catch (error) {
28
+ if (error.code !== "ENOENT") throw error;
29
+ }
30
+ await symlink(path.relative(path.dirname(target), source), target);
31
+ return "created";
32
+ }
33
+
34
+ async function sourceExists(source) {
35
+ try {
36
+ await access(source);
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ export async function installWorkspaceClis({ workspaceRoot, manifest, binDir, force = false }) {
44
+ const destination = binDir
45
+ ? path.resolve(binDir)
46
+ : path.join(process.env.HOME ?? os.homedir(), ".local", "bin");
47
+ const result = { linked: [], built: [] };
48
+
49
+ await mkdir(destination, { recursive: true });
50
+ const targets = manifest.clis.map((cli) => ({ cli, target: path.join(destination, cli.name) }));
51
+ for (const { cli, target } of targets) {
52
+ try {
53
+ const stats = await lstat(target);
54
+ if (!force) {
55
+ throw new Error(`refusing to replace existing CLI without --force: ${target}`);
56
+ }
57
+ if (stats.isDirectory()) throw new Error(`refusing to replace CLI directory: ${target}`);
58
+ } catch (error) {
59
+ if (error.code !== "ENOENT") throw error;
60
+ }
61
+ }
62
+
63
+ const backup = await mkdtemp(path.join(destination, ".deepflow-cli-backup-"));
64
+ const backedUp = [];
65
+ const touched = [];
66
+ try {
67
+ for (const { cli, target } of targets) {
68
+ try {
69
+ await lstat(target);
70
+ await rename(target, path.join(backup, cli.name));
71
+ backedUp.push(cli.name);
72
+ } catch (error) {
73
+ if (error.code !== "ENOENT") throw error;
74
+ }
75
+ }
76
+
77
+ for (const cli of manifest.clis) {
78
+ const projectRoot = path.join(workspaceRoot, cli.project);
79
+ const target = path.join(destination, cli.name);
80
+
81
+ if (cli.name === "df-workbench") {
82
+ const source = path.join(projectRoot, "cli", "df-workbench.mjs");
83
+ if (!(await sourceExists(source))) throw new Error(`missing CLI source: ${source}`);
84
+ await replaceLink(source, target);
85
+ touched.push(cli.name);
86
+ result.linked.push(cli.name);
87
+ continue;
88
+ }
89
+
90
+ if (cli.name === "df-agentrt") {
91
+ await run("make", ["build"], { cwd: projectRoot, env: process.env });
92
+ const source = path.join(projectRoot, "deploy", "bin", "df-agentrt");
93
+ if (!(await sourceExists(source))) throw new Error(`missing built CLI: ${source}`);
94
+ await replaceLink(source, target);
95
+ touched.push(cli.name);
96
+ result.built.push(cli.name);
97
+ continue;
98
+ }
99
+
100
+ await run("make", ["install-cli", `BIN_DIR=${destination}`], {
101
+ cwd: projectRoot,
102
+ env: process.env,
103
+ });
104
+ touched.push(cli.name);
105
+ result.built.push(cli.name);
106
+ }
107
+
108
+ await rm(backup, { recursive: true, force: true });
109
+ return { binDir: destination, ...result };
110
+ } catch (error) {
111
+ for (const { cli } of targets) await rm(path.join(destination, cli.name), { recursive: true, force: true });
112
+ for (const name of backedUp) await rename(path.join(backup, name), path.join(destination, name));
113
+ throw error;
114
+ } finally {
115
+ await rm(backup, { recursive: true, force: true });
116
+ }
117
+ }
118
+
119
+ export async function verifyWorkspaceClis({ workspaceRoot, manifest, binDir }) {
120
+ const destination = binDir
121
+ ? path.resolve(binDir)
122
+ : path.join(process.env.HOME ?? os.homedir(), ".local", "bin");
123
+ const missing = [];
124
+ const invalid = [];
125
+ for (const cli of manifest.clis) {
126
+ try {
127
+ const target = path.join(destination, cli.name);
128
+ const stats = await lstat(target);
129
+ await access(target);
130
+ if (stats.isDirectory()) invalid.push({ name: cli.name, reason: "target is a directory" });
131
+ } catch (error) {
132
+ if (error.code === "ENOENT") {
133
+ missing.push(cli.name);
134
+ } else {
135
+ invalid.push({ name: cli.name, reason: error.message });
136
+ }
137
+ }
138
+ }
139
+ return { binDir: destination, missing, invalid };
140
+ }
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+
3
+ import { defaultCodexSkillsDir } from "../constants.mjs";
4
+ import { verifyInstalledBundle } from "../bundle-installer.mjs";
5
+ import { verifyWorkspaceSkills } from "../linker.mjs";
6
+ import { verifyWorkspaceClis } from "../cli-install.mjs";
7
+ import { loadManifest } from "../manifest.mjs";
8
+
9
+ export async function doctor(options) {
10
+ if (options.source === "package" || options.source === "bundle") {
11
+ const bundleDir = options.bundleDir ?? path.join(path.dirname(options.manifestPath), "bundle");
12
+ const targetDir = options.targetDir ?? defaultCodexSkillsDir();
13
+ try {
14
+ const manifest = await loadManifest(options.manifestPath);
15
+ const release = await verifyInstalledBundle({ bundleDir, targetDir, expectedVersion: manifest.version });
16
+ return {
17
+ ok: true,
18
+ bundleDir,
19
+ targetDir,
20
+ release,
21
+ };
22
+ } catch (error) {
23
+ return { ok: false, bundleDir, error: error.message };
24
+ }
25
+ }
26
+ const links = await verifyWorkspaceSkills(options);
27
+ const manifest = await loadManifest(options.manifestPath);
28
+ const clis = await verifyWorkspaceClis({ ...options, manifest });
29
+ return {
30
+ ok: links.missing.length === 0 && links.invalid.length === 0 && links.sources.length === 0
31
+ && clis.missing.length === 0 && clis.invalid.length === 0,
32
+ skills: links,
33
+ clis,
34
+ };
35
+ }
@@ -0,0 +1,32 @@
1
+ import path from "node:path";
2
+
3
+ import { defaultCodexSkillsDir } from "../constants.mjs";
4
+ import { installBundleSkills } from "../bundle-installer.mjs";
5
+ import { installWorkspaceSkills } from "../linker.mjs";
6
+ import { installWorkspaceClis } from "../cli-install.mjs";
7
+ import { loadManifest } from "../manifest.mjs";
8
+
9
+ export async function install(options) {
10
+ if (options.source === "package") {
11
+ const packageRoot = path.dirname(options.manifestPath);
12
+ const manifest = await loadManifest(options.manifestPath);
13
+ const bundleDir = options.bundleDir ?? path.join(packageRoot, "bundle");
14
+ return {
15
+ skills: await installBundleSkills({
16
+ bundleDir,
17
+ targetDir: options.targetDir ?? defaultCodexSkillsDir(),
18
+ force: options.force,
19
+ expectedVersion: manifest.version,
20
+ }),
21
+ clis: { package: "@metagptx/deepflow-suite-cli" },
22
+ };
23
+ }
24
+ if (options.source !== "workspace") {
25
+ throw new Error("--source must be workspace or package");
26
+ }
27
+ const skills = await installWorkspaceSkills(options);
28
+ if (options.skipCli) return { skills, clis: { skipped: true } };
29
+ const manifest = await loadManifest(options.manifestPath);
30
+ const clis = await installWorkspaceClis({ ...options, manifest });
31
+ return { skills, clis };
32
+ }
@@ -0,0 +1,30 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export const DISTRIBUTION = "deepflow-suite";
5
+ export const PACKAGE_NAME = "@metagptx/deepflow-suite-cli";
6
+ export const CLI_NAME = "deepflow-suite";
7
+ export const RELEASE_FILE = "deepflow-release.json";
8
+
9
+ export const SKILL_NAMES = [
10
+ "deepflow-suite",
11
+ "df-workbench",
12
+ "df-task",
13
+ "df-taskdriver",
14
+ "df-agentctrl",
15
+ "df-agentrt",
16
+ "df-agentrt-repo",
17
+ ];
18
+
19
+ export const CLI_NAMES = [
20
+ "df-workbench",
21
+ "df-task",
22
+ "df-taskdriver",
23
+ "df-agentctrl",
24
+ "df-agentrt",
25
+ ];
26
+
27
+ export function defaultCodexSkillsDir() {
28
+ const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
29
+ return path.join(codexHome, "skills");
30
+ }
package/src/linker.mjs ADDED
@@ -0,0 +1,96 @@
1
+ import { lstat, mkdir, readlink, symlink, unlink, access, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { loadManifest, resolveSkillSources } from "./manifest.mjs";
5
+
6
+ async function exists(filePath) {
7
+ try {
8
+ await access(filePath);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ async function inspectTarget(targetPath) {
16
+ try {
17
+ const stats = await lstat(targetPath);
18
+ return stats.isSymbolicLink() ? "symlink" : "other";
19
+ } catch (error) {
20
+ if (error.code === "ENOENT") return "missing";
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ export async function installWorkspaceSkills({ workspaceRoot, targetDir, force = false, manifestPath }) {
26
+ const manifest = await loadManifest(manifestPath);
27
+ const skills = resolveSkillSources(manifest, workspaceRoot, "workspace");
28
+ const destination = targetDir ?? path.join(workspaceRoot, ".agents", "skills");
29
+
30
+ for (const skill of skills) {
31
+ if (!(await exists(path.join(skill.sourcePath, "SKILL.md")))) {
32
+ throw new Error(`missing Skill source: ${skill.sourcePath}/SKILL.md`);
33
+ }
34
+ const targetPath = path.join(destination, skill.name);
35
+ const kind = await inspectTarget(targetPath);
36
+ if (kind === "other" && !force) {
37
+ throw new Error(`refusing to replace non-symlink Skill target: ${targetPath}`);
38
+ }
39
+ }
40
+
41
+ await mkdir(destination, { recursive: true });
42
+ const result = { created: [], updated: [], unchanged: [] };
43
+ for (const skill of skills) {
44
+ const targetPath = path.join(destination, skill.name);
45
+ const relativeSource = path.relative(destination, skill.sourcePath);
46
+ const kind = await inspectTarget(targetPath);
47
+ if (kind === "symlink") {
48
+ const current = await readlink(targetPath);
49
+ const currentAbsolute = path.resolve(destination, current);
50
+ if (currentAbsolute === skill.sourcePath) {
51
+ result.unchanged.push(skill.name);
52
+ continue;
53
+ }
54
+ await unlink(targetPath);
55
+ await symlink(relativeSource, targetPath, "dir");
56
+ result.updated.push(skill.name);
57
+ continue;
58
+ }
59
+ if (kind === "other") {
60
+ if (!force) throw new Error(`refusing to replace non-symlink Skill target: ${targetPath}`);
61
+ await rm(targetPath, { recursive: true, force: true });
62
+ }
63
+ await symlink(relativeSource, targetPath, "dir");
64
+ result.created.push(skill.name);
65
+ }
66
+ return result;
67
+ }
68
+
69
+ export async function verifyWorkspaceSkills({ workspaceRoot, targetDir, manifestPath }) {
70
+ const manifest = await loadManifest(manifestPath);
71
+ const skills = resolveSkillSources(manifest, workspaceRoot, "workspace");
72
+ const destination = targetDir ?? path.join(workspaceRoot, ".agents", "skills");
73
+ const missing = [];
74
+ const invalid = [];
75
+ const sources = [];
76
+ for (const skill of skills) {
77
+ if (!(await exists(path.join(skill.sourcePath, "SKILL.md")))) {
78
+ sources.push({ name: skill.name, path: skill.sourcePath });
79
+ }
80
+ const targetPath = path.join(destination, skill.name);
81
+ const kind = await inspectTarget(targetPath);
82
+ if (kind === "missing") {
83
+ missing.push(skill.name);
84
+ continue;
85
+ }
86
+ if (kind !== "symlink") {
87
+ invalid.push({ name: skill.name, reason: "target is not a symlink" });
88
+ continue;
89
+ }
90
+ const current = path.resolve(destination, await readlink(targetPath));
91
+ if (current !== skill.sourcePath) {
92
+ invalid.push({ name: skill.name, reason: `points to ${current}` });
93
+ }
94
+ }
95
+ return { missing, invalid, sources };
96
+ }