Author SHA1 Message Date
oleg-lukasonokandClaude Code 9b84db4b0a Fix skill origin metadata and macOS source-importer portability
Point SKILL.md origin-repository/origin-path at the renamed
diagrams-drawio repository and workspace path.

openConfined() and the directory walker addressed opened files through
/proc/self/fd, which only exists on Linux, so every file/directory source
import failed on macOS (2 of 150 tests). Keep the procfs path where it is
available and fall back to realpath() plus a dev/inode identity check
against the open handle elsewhere.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-06 12:58:12 +03:00
jarvis-at-skic 9809cf73e9 Merge pull request 'refactor: rename skill to diagrams-drawio' (#3) from refactor/rename-diagrams-drawio into test 2026-09-03 13:11:25 -07:00
jarvis-at-skic 1550609e5b [verified] refactor: rename skill to diagrams-drawio 2026-09-03 20:10:54 +00:00
jarvis-at-skic aec73fdfe8 feat: add integrated diagram lifecycle and capability toolbox (#2)
Reviewed tree 0d81980cafc94b90bd54f88daa27a271d8c69d2e; 150/150 tests passed.
2026-09-03 12:33:25 -07:00
11 changed files with 52 additions and 21 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# drawio-main # diagrams-drawio
TypeScript tools for deterministic Diagram IR-to-Draw.io generation, loss-aware round trips, transactional editing, semantic analysis, synchronization, offline publishing, and visual verification. TypeScript tools for deterministic Diagram IR-to-Draw.io generation, loss-aware round trips, transactional editing, semantic analysis, synchronization, offline publishing, and visual verification.
+4 -3
View File
@@ -1,13 +1,14 @@
--- ---
name: drawio-main name: diagrams-drawio
description: Always use when user asks to create, generate, draw, or design a diagram, flowchart, architecture diagram, ER diagram, sequence diagram, class diagram, network diagram, mockup, wireframe, or UI sketch, or mentions draw.io, drawio, drawoi, .drawio files, or diagram export to PNG/SVG/PDF. description: Always use when user asks to create, generate, draw, or design a diagram, flowchart, architecture diagram, ER diagram, sequence diagram, class diagram, network diagram, mockup, wireframe, or UI sketch, or mentions draw.io, drawio, drawoi, .drawio files, or diagram export to PNG/SVG/PDF.
license: Proprietary license: Proprietary
metadata: metadata:
author: workspace-swiss-knife author: workspace-swiss-knife
version: "2.0" version: "2.0"
spec: agentskills.io/specification spec: agentskills.io/specification
origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/drawio-main.git origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/diagrams-drawio.git
origin-path: $HOME/projects-ibm/cognitive-architect/workspace-skills-code-agent/drawio-main origin-path: $HOME/projects-skills-code-agent/ws-skills-code-agent/diagrams-drawio
repository: https://gitea.lego-cloud.eu/home-v1-skills-code-agent/diagrams-drawio
compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments
--- ---
+1 -1
View File
@@ -1,7 +1,7 @@
version: 2 version: 2
title: Platform views title: Platform views
provenance: provenance:
source: drawio-main example source: diagrams-drawio example
pages: pages:
- id: system - id: system
title: System title: System
+1 -1
View File
@@ -1,4 +1,4 @@
# drawio-main — Capabilities # diagrams-drawio — Capabilities
This file lists all capabilities an agent can use from this skill. This file lists all capabilities an agent can use from this skill.
+2 -2
View File
@@ -1,4 +1,4 @@
# drawio-main — Maintenance Guide # diagrams-drawio — Maintenance Guide
This document is for **developers** maintaining or extending the `drawio-tools` CLI. This document is for **developers** maintaining or extending the `drawio-tools` CLI.
For agent usage instructions, see [SKILL.md](../SKILL.md). For agent usage instructions, see [SKILL.md](../SKILL.md).
@@ -45,7 +45,7 @@ and `$HOME/.claude/skills`, so the repo working copy stays the single source of
```bash ```bash
cd <skill-manager>/scripts cd <skill-manager>/scripts
task deploy -- --skill-dir="/absolute/path/to/drawio-main" task deploy -- --skill-dir="/absolute/path/to/diagrams-drawio"
``` ```
Verify the CLI works after installation: Verify the CLI works after installation:
+1 -1
View File
@@ -1,4 +1,4 @@
# drawio-main — Connector Routing Best Practices # diagrams-drawio — Connector Routing Best Practices
## Connector Routing Best Practices (Zero-Overlap Guarantee) ## Connector Routing Best Practices (Zero-Overlap Guarantee)
+1 -1
View File
@@ -1,4 +1,4 @@
# drawio-main — Layout Rules # diagrams-drawio — Layout Rules
Connector placement and layout rules for draw.io diagrams generated by this skill. Connector placement and layout rules for draw.io diagrams generated by this skill.
These rules are **MANDATORY** — apply them when generating any diagram. These rules are **MANDATORY** — apply them when generating any diagram.
+1 -1
View File
@@ -1,4 +1,4 @@
# drawio-main — Style Rules # diagrams-drawio — Style Rules
Visual appearance and sizing rules for draw.io diagrams generated by this skill. Visual appearance and sizing rules for draw.io diagrams generated by this skill.
These rules are **MANDATORY** — apply them when generating any diagram. These rules are **MANDATORY** — apply them when generating any diagram.
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://gitea.lego-cloud.eu/home-v1-skills-code-agent/drawio-main/raw/branch/main/schemas/diagram-ir-v2.schema.json", "$id": "https://gitea.lego-cloud.eu/home-v1-skills-code-agent/diagrams-drawio/raw/branch/test/schemas/diagram-ir-v2.schema.json",
"title": "drawio-main Diagram IR v2", "title": "diagrams-drawio Diagram IR v2",
"type": "object", "type": "object",
"required": ["version", "pages"], "required": ["version", "pages"],
"properties": { "properties": {
+1 -1
View File
@@ -83,7 +83,7 @@ test("build serializes every v2 page and honors explicit geometry and waypoints"
const input = join(dir, "platform.yaml"); const input = join(dir, "platform.yaml");
const output = join(dir, "platform.drawio"); const output = join(dir, "platform.drawio");
try { try {
writeFileSync(input, `version: 2\ntitle: Platform\ntheme: zinc\nprovenance: { source: test }\nproperties: { audience: engineering }\nextensions: { owner: drawio-main }\npages:\n - id: system\n title: System\n properties: { view: logical }\n layout: { type: manual }\n nodes:\n - id: client\n label: Client\n kind: actor\n properties: { tier: edge }\n provenance: { source: inventory }\n extensions: { critical: true }\n geometry: { x: 40, y: 80, width: 120, height: 60 }\n - id: api\n label: API\n geometry: { x: 300, y: 80, width: 120, height: 60 }\n edges:\n - id: call\n source: client\n target: api\n kind: request\n properties: { protocol: HTTPS }\n waypoints: [{ x: 230, y: 110 }]\n - id: deployment\n title: Deployment\n nodes:\n - { id: server, label: Server }\n edges: []\n`, "utf8"); writeFileSync(input, `version: 2\ntitle: Platform\ntheme: zinc\nprovenance: { source: test }\nproperties: { audience: engineering }\nextensions: { owner: diagrams-drawio }\npages:\n - id: system\n title: System\n properties: { view: logical }\n layout: { type: manual }\n nodes:\n - id: client\n label: Client\n kind: actor\n properties: { tier: edge }\n provenance: { source: inventory }\n extensions: { critical: true }\n geometry: { x: 40, y: 80, width: 120, height: 60 }\n - id: api\n label: API\n geometry: { x: 300, y: 80, width: 120, height: 60 }\n edges:\n - id: call\n source: client\n target: api\n kind: request\n properties: { protocol: HTTPS }\n waypoints: [{ x: 230, y: 110 }]\n - id: deployment\n title: Deployment\n nodes:\n - { id: server, label: Server }\n edges: []\n`, "utf8");
const result = run(input, 0, output); const result = run(input, 0, output);
assert.deepEqual(result.summary, { nodes: 3, edges: 1, pages: 2 }); assert.deepEqual(result.summary, { nodes: 3, edges: 1, pages: 2 });
const xml = readFileSync(output, "utf8"); const xml = readFileSync(output, "utf8");
+37 -7
View File
@@ -1,5 +1,5 @@
import { constants } from "node:fs"; import { constants } from "node:fs";
import { open, readdir, realpath } from "node:fs/promises"; import { open, readdir, realpath, stat } from "node:fs/promises";
import type { FileHandle } from "node:fs/promises"; import type { FileHandle } from "node:fs/promises";
import { isAbsolute, relative, resolve } from "node:path"; import { isAbsolute, relative, resolve } from "node:path";
@@ -643,7 +643,7 @@ async function openConfined(candidate: string, root: string): Promise<{ handle:
throw error; throw error;
} }
try { try {
const canonical = await realpath(`/proc/self/fd/${handle.fd}`); const canonical = await canonicalPathOfOpenHandle(handle, candidate);
if (!isConfined(root, canonical)) throw new Error("Opened source escapes outside declared root"); if (!isConfined(root, canonical)) throw new Error("Opened source escapes outside declared root");
return { handle, canonical }; return { handle, canonical };
} catch (error) { } catch (error) {
@@ -652,6 +652,35 @@ async function openConfined(candidate: string, root: string): Promise<{ handle:
} }
} }
let procFdAvailable: boolean | undefined;
/** Path to address an open directory by: its `/proc/self/fd/N` entry on Linux, else its verified canonical path. */
async function descriptorPath(handle: FileHandle, canonical: string): Promise<string> {
if (procFdAvailable === undefined) {
try { await realpath(`/proc/self/fd/${handle.fd}`); procFdAvailable = true; } catch { procFdAvailable = false; }
}
return procFdAvailable ? `/proc/self/fd/${handle.fd}` : canonical;
}
/**
* Resolve the canonical path of an already-opened file. On Linux `/proc/self/fd/N` answers for the
* exact descriptor; on macOS/BSD (no procfs) fall back to resolving the candidate path and proving it
* names the same inode as the open handle, so a swap between open() and realpath() is still rejected.
*/
async function canonicalPathOfOpenHandle(handle: FileHandle, candidate: string): Promise<string> {
try {
return await realpath(`/proc/self/fd/${handle.fd}`);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const canonical = await realpath(candidate);
const [opened, resolved] = await Promise.all([handle.stat(), stat(canonical)]);
if (opened.dev !== resolved.dev || opened.ino !== resolved.ino) {
throw new Error("Opened source does not match its canonical path");
}
return canonical;
}
async function readBounded(handle: FileHandle, limit: number, directory: boolean): Promise<Buffer> { async function readBounded(handle: FileHandle, limit: number, directory: boolean): Promise<Buffer> {
const initial = await handle.stat(); const initial = await handle.stat();
if (!initial.isFile()) throw new Error("Source entry must be a regular file"); if (!initial.isFile()) throw new Error("Source entry must be a regular file");
@@ -711,12 +740,13 @@ export async function importSource(request: SourceImportRequest): Promise<Source
const results: SourceImportResult[] = []; const results: SourceImportResult[] = [];
let consumed = 0; let consumed = 0;
const walk = async (directory: FileHandle): Promise<void> => { const walk = async (directory: { handle: FileHandle; canonical: string }): Promise<void> => {
for (const name of (await readdir(`/proc/self/fd/${directory.fd}`)).sort(codePointCompare)) { const base = await descriptorPath(directory.handle, directory.canonical);
const opened = await openConfined(resolve(`/proc/self/fd/${directory.fd}`, name), rootPath); for (const name of (await readdir(base)).sort(codePointCompare)) {
const opened = await openConfined(resolve(base, name), rootPath);
try { try {
const entryInfo = await opened.handle.stat(); const entryInfo = await opened.handle.stat();
if (entryInfo.isDirectory()) await walk(opened.handle); if (entryInfo.isDirectory()) await walk(opened);
else if (entryInfo.isFile()) { else if (entryInfo.isFile()) {
const dot = name.lastIndexOf("."); const extension = dot >= 0 ? name.slice(dot).toLowerCase() : ""; const dot = name.lastIndexOf("."); const extension = dot >= 0 ? name.slice(dot).toLowerCase() : "";
if (extensions[request.sourceKind].has(extension)) { if (extensions[request.sourceKind].has(extension)) {
@@ -729,7 +759,7 @@ export async function importSource(request: SourceImportRequest): Promise<Source
} finally { await opened.handle.close(); } } finally { await opened.handle.close(); }
} }
}; };
try { await walk(source.handle); } try { await walk(source); }
finally { await source.handle.close(); } finally { await source.handle.close(); }
if (results.length === 0) throw new Error(`No supported ${request.sourceKind} source files found`); if (results.length === 0) throw new Error(`No supported ${request.sourceKind} source files found`);
const nodeById = new Map<string, DiagramNode>(); const edgeById = new Map<string, DiagramEdge>(); const diagnostics: ImportDiagnostic[] = []; const nodeById = new Map<string, DiagramNode>(); const edgeById = new Map<string, DiagramEdge>(); const diagnostics: ImportDiagnostic[] = [];