Skip to content
Merged
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
12 changes: 12 additions & 0 deletions extension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to the **AI Stack Kit** VS Code extension are documented here.

## [1.3.6] - 2026-07-03

### Changed

- **Pass-through module install** — skills, subagents, and hooks are copied from source as authored (no auto-generated prompts or agent stubs).
- **Faster sync** — parallel module fetch/install, GitHub tarball caching for shared repos, parallel project + profile sync, and parallel adapter file writes.
- **Profile installs** — auto-sync after catalog add, watcher on `~/.aistack/spec.yaml`, and proactive creation of client `skills/`, `agents/`, and `hooks/` directories on first user-scope sync.

### Fixed

- Profile-level skill installs now materialize under `~/.cursor/skills/` (and Claude/Copilot equivalents) even when those folders did not exist before.

## [1.3.5] - 2026-05-24

### Changed
Expand Down
2 changes: 1 addition & 1 deletion extension/media/catalog.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions extension/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "ai-stack-kit",
"displayName": "AI Stack Kit",
"description": "Manage AI skills, subagents, and hooks from your IDE — search catalogs, sync to Cursor, Copilot, and Claude.",
"version": "1.3.5",
"version": "1.3.6",
"publisher": "deb-adarsh",
"license": "Apache-2.0",
"repository": {
Expand Down
28 changes: 24 additions & 4 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ function activateExtension(context: vscode.ExtensionContext): void {
specWatcher.onDidCreate(() => void onSpecChanged());
context.subscriptions.push(specWatcher);

const profileSpec = userSpecPath();
const profileSpecWatcher = vscode.workspace.createFileSystemWatcher(profileSpec);
profileSpecWatcher.onDidChange(() => void onSpecChanged());
profileSpecWatcher.onDidCreate(() => void onSpecChanged());
context.subscriptions.push(profileSpecWatcher);

void updateStatusBar();

vscode.workspace.onDidChangeWorkspaceFolders(() => {
Expand Down Expand Up @@ -356,16 +362,29 @@ async function doRunSync(silent: boolean): Promise<void> {
cancellable: false,
},
async () => {
const syncTasks: Promise<ApplyPipelineResult | undefined>[] = [];
if (hasProject && projectWs) {
log('info', 'Syncing project spec…');
projectResult = await projectWs.syncWithLogger(syncOpts, log);
logScopeResult('project', projectResult);
syncTasks.push(projectWs.syncWithLogger(syncOpts, log).then((result) => {
logScopeResult('project', result);
return result;
}));
}
if (hasProfile) {
log('info', 'Syncing profile spec…');
const profileWs = getProfileWorkspace();
profileResult = await profileWs.syncWithLogger(syncOpts, log);
logScopeResult('profile', profileResult);
syncTasks.push(profileWs.syncWithLogger(syncOpts, log).then((result) => {
logScopeResult('profile', result);
return result;
}));
}
const results = await Promise.all(syncTasks);
let resultIndex = 0;
if (hasProject && projectWs) {
projectResult = results[resultIndex++];
}
if (hasProfile) {
profileResult = results[resultIndex++];
}
}
);
Expand Down Expand Up @@ -539,6 +558,7 @@ async function runSearch(): Promise<void> {
`Added ${picked.hit.name} to ${target} spec`
);
refreshAll();
void vscode.commands.executeCommand('aistack.sync');
} catch (e) {
void vscode.window.showErrorMessage(e instanceof Error ? e.message : String(e));
}
Expand Down
1 change: 1 addition & 0 deletions extension/src/views/catalogWebviewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class CatalogWebviewProvider implements vscode.WebviewViewProvider {
text: `Added to ${target} spec`,
});
void vscode.commands.executeCommand('aistack.modules.refresh');
void vscode.commands.executeCommand('aistack.sync');
} catch (e) {
void vscode.window.showErrorMessage(e instanceof Error ? e.message : String(e));
}
Expand Down
1 change: 1 addition & 0 deletions extension/templates/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ client:
# Features to enable
features:
- skills
- agents
- hooks

# Client-specific settings (optional)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@deb-adarsh/ai-stack-kit",
"version": "1.2.0",
"version": "1.3.6",
"description": "AI Stack Kit — spec-driven skills, subagents, and hooks for Cursor, Copilot, and Claude (CLI + VS Code extension)",
"type": "module",
"main": "dist/index.js",
Expand Down Expand Up @@ -37,7 +37,7 @@
"package:extension": "npm run build:extension && cd extension && npm run vsix",
"dev": "tsc --watch",
"start": "node dist/cli/index.js",
"test": "npm run build && node --test dist/cli/__tests__/get-module-versions.test.js dist/pipeline/__tests__/spec-loader.test.js dist/pipeline/__tests__/apply-dry-run.test.js dist/client-adapters/__tests__/adapter-factory.test.js dist/client-adapters/__tests__/merge-json.test.js dist/api/__tests__/workspace-api.test.js dist/api/__tests__/profile-spec.test.js",
"test": "npm run build && node --test dist/cli/__tests__/get-module-versions.test.js dist/pipeline/__tests__/spec-loader.test.js dist/pipeline/__tests__/apply-dry-run.test.js dist/client-adapters/__tests__/adapter-factory.test.js dist/client-adapters/__tests__/merge-json.test.js dist/client-adapters/__tests__/ensure-client-dirs.test.js dist/client-adapters/__tests__/partition-modules.test.js dist/api/__tests__/workspace-api.test.js dist/api/__tests__/profile-spec.test.js",
"lint": "eslint src --ext .ts",
"format": "prettier --write \"src/**/*.ts\""
},
Expand Down
2 changes: 1 addition & 1 deletion src/api/profile-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function ensureProfileSpec(options: EnsureProfileSpecOptions = {}):
client: {
type: clientType,
installScope: 'user',
features: ['skills', 'hooks'],
features: ['skills', 'agents', 'hooks'],
},
skills: [],
modules: [],
Expand Down
14 changes: 12 additions & 2 deletions src/api/workspace-facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,29 @@ export async function syncAllScopes(
options: SyncOptions = {}
): Promise<DualSyncResult> {
const result: DualSyncResult = {};
const tasks: Promise<void>[] = [];

if (projectRoot) {
const projectWs = getProjectWorkspace(projectRoot);
if (projectWs.hasSpec()) {
result.project = await projectWs.sync(options);
tasks.push(
projectWs.sync(options).then((syncResult) => {
result.project = syncResult;
})
);
}
}

if (hasProfileSpec()) {
const profileWs = getProfileWorkspace();
result.profile = await profileWs.sync(options);
tasks.push(
profileWs.sync(options).then((syncResult) => {
result.profile = syncResult;
})
);
}

await Promise.all(tasks);
return result;
}

Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ export async function createSpecFile(
},
client: {
type: data.client as any,
features: ['skills', 'hooks'],
features: ['skills', 'agents', 'hooks'],
},
skills: data.skills.map((name) => {
const gh = resolveSuggestionGithubSource(name);
Expand Down
57 changes: 57 additions & 0 deletions src/client-adapters/__tests__/ensure-client-dirs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { ensureClientInstallDirs } from '../ensure-client-dirs.js';

describe('ensureClientInstallDirs', () => {
let priorHome: string | undefined;
let fakeHome: string;

beforeEach(async () => {
priorHome = process.env.HOME;
fakeHome = await mkdtemp(path.join(tmpdir(), 'aistack-home-'));
process.env.HOME = fakeHome;
});

afterEach(async () => {
if (priorHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = priorHome;
}
await rm(fakeHome, { recursive: true, force: true });
});

it('creates skills, agents, and hooks dirs on first user-scope sync', async () => {
const skillsDir = path.join(fakeHome, '.cursor', 'skills');
const agentsDir = path.join(fakeHome, '.cursor', 'agents');
const hooksDir = path.join(fakeHome, '.cursor', 'hooks');
assert.equal(existsSync(skillsDir), false);

const created = await ensureClientInstallDirs(
{ type: 'cursor', installScope: 'user' },
path.join(fakeHome, '.aistack')
);

assert.ok(created.includes(skillsDir));
assert.ok(existsSync(agentsDir));
assert.ok(existsSync(hooksDir));
});

it('creates ~/.copilot/skills and ~/.claude/skills for user scope', async () => {
await ensureClientInstallDirs(
{ type: 'copilot', installScope: 'user' },
path.join(fakeHome, '.aistack')
);
await ensureClientInstallDirs(
{ type: 'claude', installScope: 'user' },
path.join(fakeHome, '.aistack')
);

assert.ok(existsSync(path.join(fakeHome, '.copilot', 'skills')));
assert.ok(existsSync(path.join(fakeHome, '.claude', 'skills')));
});
});
52 changes: 52 additions & 0 deletions src/client-adapters/__tests__/partition-modules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { CursorClientAdapter } from '../cursor/cursor-adapter.js';
import { partitionModulesByType } from '../emit-skill-agent-files.js';
import type { ResolvedSkill } from '../normalized.js';

function module(name: string, moduleType: string, files: Record<string, string>): ResolvedSkill {
return {
id: `${name}@1`,
name,
version: '1',
files,
manifest: null,
metadata: { moduleType },
};
}

describe('partitionModulesByType', () => {
it('routes skills, subagents, and hooks to separate buckets', () => {
const modules = [
module('my-skill', 'skill', { 'SKILL.md': '# skill' }),
module('reviewer', 'subagent', { 'reviewer.agent.md': '# agent' }),
module('fmt-hook', 'hook', { 'hook.json': '{}' }),
];
const parts = partitionModulesByType(modules);
assert.equal(parts.skills.length, 1);
assert.equal(parts.subagents.length, 1);
assert.equal(parts.hooks.length, 1);
assert.equal(parts.skills[0].name, 'my-skill');
assert.equal(parts.subagents[0].name, 'reviewer');
});
});

describe('CursorClientAdapter pass-through', () => {
it('writes fetched files to skills and agents dirs without synthesis', () => {
const adapter = new CursorClientAdapter();
const output = adapter.generateConfig({
modules: [
module('canvas', 'skill', { 'SKILL.md': '# Canvas skill' }),
module('planner', 'subagent', { 'planner.md': '# Planner agent' }),
],
metadata: { specVersion: '1.0', generatedAt: '2026-01-01T00:00:00.000Z' },
client: { type: 'cursor', installScope: 'project' },
spec: { version: '1.0' },
});

const paths = output.files.map((f) => f.path);
assert.ok(paths.some((p) => p.includes('.cursor/skills/canvas/SKILL.md')));
assert.ok(paths.some((p) => p.includes('.cursor/agents/planner/planner.md')));
assert.ok(!paths.some((p) => p.includes('.cursor/prompts/')));
});
});
20 changes: 13 additions & 7 deletions src/client-adapters/apply-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export async function applyAdapterOutput(
const merged: string[] = [];
const conflicts: { path: string; message: string }[] = [];
const adapterRoot = options.adapterFilesystemRoot ?? projectPath;
const directWrites: Promise<void>[] = [];

for (const file of output.files) {
const root = file.pathAnchor === 'project' ? projectPath : adapterRoot;
Expand All @@ -34,8 +35,11 @@ export async function applyAdapterOutput(
}

if (file.mergeStrategy === 'overwrite' || file.managed !== false) {
await writeFile(abs, file.content, 'utf-8');
written.push(file.path);
directWrites.push(
writeFile(abs, file.content, 'utf-8').then(() => {
written.push(file.path);
})
);
continue;
}

Expand All @@ -49,9 +53,6 @@ export async function applyAdapterOutput(
const base = parseJsonSafe(existing);
const patch = parseJsonSafe(file.content);
if (!base || !patch) {
// Writing conflict markers into a .json file would break clients that
// parse it (e.g. VS Code's settings.json). Record as a conflict and
// skip the write instead so the user can resolve it manually.
conflicts.push({
path: file.path,
message: !base
Expand Down Expand Up @@ -83,11 +84,16 @@ export async function applyAdapterOutput(
}
}
} catch {
await writeFile(abs, file.content, 'utf-8');
written.push(file.path);
directWrites.push(
writeFile(abs, file.content, 'utf-8').then(() => {
written.push(file.path);
})
);
}
}

await Promise.all(directWrites);

return { written, skipped, merged, conflicts: conflicts.length ? conflicts : undefined };
}

Expand Down
Loading
Loading