Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/init.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cp, mkdir, readdir, readFile, writeFile, stat } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { join, dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { createPrompt } from './prompt.js';
Expand Down Expand Up @@ -154,8 +154,9 @@ export async function loadSavedLocale(targetDir) {
async function installAllSkills(targetDir) {
const available = await listAvailable();
for (const id of available) {
await installSkill(id, targetDir);
console.log(` ${t('createdFile', { path: `skills/${id}/SKILL.md` })}`);
const skillDir = await installSkill(id, targetDir);
const skillPath = join(relative(targetDir, skillDir), 'SKILL.md').replaceAll('\\', '/');
console.log(` ${t('createdFile', { path: skillPath })}`);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/readme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ O Escritório Virtual é uma interface visual 2D que mostra seus agentes trabalh

```bash
npx serve squads/<nome-do-squad>/dashboard
# ou, se seu projeto usa layout por departamento:
npx serve marketing/squads/<nome-do-squad>/dashboard
```

**Passo 3 —** Abra `http://localhost:3000` no seu navegador.
Expand Down Expand Up @@ -114,6 +116,8 @@ The Virtual Office is a 2D visual interface that shows your agents working in re

```bash
npx serve squads/<squad-name>/dashboard
# or, if your project uses a department-based layout:
npx serve marketing/squads/<squad-name>/dashboard
```

**Step 3 —** Open `http://localhost:3000` in your browser.
119 changes: 86 additions & 33 deletions src/runs.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,111 @@
import { readdir, readFile } from 'node:fs/promises';
import { access, readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';

const MAX_RUNS = 20;
const PROJECT_SQUADS_DIR_PARTS = [
['marketing', 'squads'],
['squads'],
];

export async function listRuns(squadName, targetDir = process.cwd()) {
const squadsDir = join(targetDir, 'squads');
let squadNames;

async function pathExists(path) {
try {
if (squadName) {
squadNames = [squadName];
} else {
const entries = await readdir(squadsDir, { withFileTypes: true });
squadNames = entries.filter((e) => e.isDirectory()).map((e) => e.name);
}
await access(path);
return true;
} catch {
return [];
return false;
}
}

const runs = [];
function getProjectSquadsDirCandidates(targetDir) {
return PROJECT_SQUADS_DIR_PARTS.map((parts) => join(targetDir, ...parts));
}

for (const name of squadNames) {
const outputDir = join(squadsDir, name, 'output');
let runDirs;
async function getExistingProjectSquadsDirs(targetDir) {
const dirs = [];
for (const dir of getProjectSquadsDirCandidates(targetDir)) {
if (await pathExists(dir)) {
dirs.push(dir);
}
}
return dirs;
}

function isRunDirName(name) {
return /^\d{4}-\d{2}-\d{2}-\d{6}(?:-\d+)?$/.test(name);
}

async function collectProjectSquadDirs(targetDir, squadName) {
const squadDirsByName = new Map();

for (const squadsDir of await getExistingProjectSquadsDirs(targetDir)) {
if (squadName) {
const squadDir = join(squadsDir, squadName);
if (!await pathExists(squadDir)) continue;
squadDirsByName.set(squadName, [...(squadDirsByName.get(squadName) || []), squadDir]);
continue;
}

let entries;
try {
const entries = await readdir(outputDir, { withFileTypes: true });
runDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
entries = await readdir(squadsDir, { withFileTypes: true });
} catch {
continue;
}

for (const runId of runDirs) {
const run = { squad: name, runId, status: 'unknown', steps: null, duration: null };
for (const entry of entries) {
if (!entry.isDirectory()) continue;
squadDirsByName.set(entry.name, [
...(squadDirsByName.get(entry.name) || []),
join(squadsDir, entry.name),
]);
}
}

return squadDirsByName;
}

export async function listRuns(squadName, targetDir = process.cwd()) {
const squadDirsByName = await collectProjectSquadDirs(targetDir, squadName);
const runs = new Map();

for (const [name, squadDirs] of squadDirsByName) {
for (const squadDir of squadDirs) {
const outputDir = join(squadDir, 'output');
let runDirs;
try {
const raw = await readFile(join(outputDir, runId, 'state.json'), 'utf-8');
const state = JSON.parse(raw);
run.status = state.status || 'unknown';
if (state.step) run.steps = `${state.step.current}/${state.step.total}`;
if (state.startedAt && (state.completedAt || state.failedAt)) {
const start = new Date(state.startedAt).getTime();
const end = new Date(state.completedAt || state.failedAt).getTime();
run.duration = formatDuration(end - start);
}
const entries = await readdir(outputDir, { withFileTypes: true });
runDirs = entries.filter((e) => e.isDirectory() && isRunDirName(e.name)).map((e) => e.name);
} catch {
// No state.json or malformed — keep defaults
continue;
}

runs.push(run);
for (const runId of runDirs) {
const runKey = `${name}:${runId}`;
if (runs.has(runKey)) continue;

const run = { squad: name, runId, status: 'unknown', steps: null, duration: null };

try {
const raw = await readFile(join(outputDir, runId, 'state.json'), 'utf-8');
const state = JSON.parse(raw);
run.status = state.status || 'unknown';
if (state.step) run.steps = `${state.step.current}/${state.step.total}`;
if (state.startedAt && (state.completedAt || state.failedAt)) {
const start = new Date(state.startedAt).getTime();
const end = new Date(state.completedAt || state.failedAt).getTime();
run.duration = formatDuration(end - start);
}
} catch {
// No state.json or malformed — keep defaults
}

runs.set(runKey, run);
}
}
}

runs.sort((a, b) => b.runId.localeCompare(a.runId));
return runs.slice(0, MAX_RUNS);
const sortedRuns = [...runs.values()].sort((a, b) => b.runId.localeCompare(a.runId));
return sortedRuns.slice(0, MAX_RUNS);
}

export function formatDuration(ms) {
Expand Down
83 changes: 69 additions & 14 deletions src/skills.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,71 @@
import { cp, readdir, readFile, rm, stat } from 'node:fs/promises';
import { access, cp, readdir, readFile, rm, stat } from 'node:fs/promises';
import { dirname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const BUNDLED_SKILLS_DIR = join(__dirname, '..', 'skills');
const PROJECT_SKILLS_DIR_PARTS = [
['marketing', 'skills'],
['skills'],
];

const metaCache = new Map();

export async function listInstalled(targetDir) {
async function pathExists(path) {
try {
const skillsDir = join(targetDir, 'skills');
await access(path);
return true;
} catch {
return false;
}
}

function getProjectSkillsDirCandidates(targetDir) {
return PROJECT_SKILLS_DIR_PARTS.map((parts) => join(targetDir, ...parts));
}

async function getExistingProjectSkillsDirs(targetDir) {
const dirs = [];
for (const dir of getProjectSkillsDirCandidates(targetDir)) {
if (await pathExists(dir)) {
dirs.push(dir);
}
}
return dirs;
}

async function resolvePreferredProjectSkillsDir(targetDir) {
const marketingDir = join(targetDir, 'marketing');
const marketingSkillsDir = join(marketingDir, 'skills');
if (await pathExists(marketingDir) || await pathExists(marketingSkillsDir)) {
return marketingSkillsDir;
}
return join(targetDir, 'skills');
}

async function resolveInstalledSkillDir(id, targetDir) {
for (const skillsDir of await getExistingProjectSkillsDirs(targetDir)) {
const skillDir = join(skillsDir, id);
if (await pathExists(skillDir)) {
return skillDir;
}
}
return null;
}

export async function listInstalled(targetDir) {
const installed = new Set();

for (const skillsDir of await getExistingProjectSkillsDirs(targetDir)) {
const entries = await readdir(skillsDir, { withFileTypes: true });
return entries
.filter((e) => e.isDirectory() && e.name !== 'opensquad-skill-creator')
.map((e) => e.name);
} catch (err) {
if (err.code === 'ENOENT') return [];
throw err;
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'opensquad-skill-creator') {
installed.add(entry.name);
}
}
}

return [...installed];
}

export async function listAvailable() {
Expand Down Expand Up @@ -103,20 +151,25 @@ export async function installSkill(id, targetDir) {
if (err.code === 'ENOENT') throw new Error(`Skill '${id}' not found in registry`, { cause: err });
throw err;
}
const destDir = join(targetDir, 'skills', id);
const existingSkillDir = await resolveInstalledSkillDir(id, targetDir);
const preferredSkillsDir = await resolvePreferredProjectSkillsDir(targetDir);
const destDir = existingSkillDir || join(preferredSkillsDir, id);
const resolvedSrc = resolve(srcDir);
const resolvedDest = resolve(destDir);
if (resolvedSrc === resolvedDest || resolvedDest.startsWith(resolvedSrc + sep)) {
return;
return destDir;
}
await cp(srcDir, destDir, { recursive: true });
metaCache.delete(id);
return destDir;
}

export async function removeSkill(id, targetDir) {
validateSkillId(id);
const skillDir = join(targetDir, 'skills', id);
await rm(skillDir, { recursive: true, force: true });
for (const skillsDir of getProjectSkillsDirCandidates(targetDir)) {
const skillDir = join(skillsDir, id);
await rm(skillDir, { recursive: true, force: true });
}
metaCache.delete(id);
}

Expand All @@ -126,7 +179,9 @@ export function clearMetaCache() {

export async function getSkillVersion(id, targetDir) {
try {
const skillPath = join(targetDir, 'skills', id, 'SKILL.md');
const skillDir = await resolveInstalledSkillDir(id, targetDir);
if (!skillDir) return null;
const skillPath = join(skillDir, 'SKILL.md');
const content = await readFile(skillPath, 'utf-8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!fmMatch) return null;
Expand Down
5 changes: 3 additions & 2 deletions src/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ export async function update(targetDir) {
const meta = await getSkillMeta(id);
if (!meta) continue;
if (meta.type === 'mcp' || meta.type === 'hybrid') continue;
await installSkill(id, targetDir);
console.log(` ${t('createdFile', { path: `skills/${id}/SKILL.md` })}`);
const skillDir = await installSkill(id, targetDir);
const skillPath = join(relative(targetDir, skillDir), 'SKILL.md').replaceAll('\\', '/');
console.log(` ${t('createdFile', { path: skillPath })}`);
count++;
}

Expand Down
Loading