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
7 changes: 4 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ jobs:
working-directory: packages/util
run: ${{ github.workspace }}/.github/scripts/publish-if-new.sh "@aictrl/util"

- name: Publish @aictrl/plugin
working-directory: packages/plugin
run: ${{ github.workspace }}/.github/scripts/publish-if-new.sh "@aictrl/plugin"
# Note: @aictrl/plugin (workspace at packages/plugin) is the first-party
# plugin-author SDK used internally by the CLI and .opencode/tool/. It is
# marked "private": true and is NOT published to npm. Plugin types ship
# embedded in the compiled CLI binary at runtime.

- name: Publish @aictrl/sdk
working-directory: packages/sdk
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
- `packages/aictrl/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
- `packages/app`: The shared web UI components, written in SolidJS
- `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`)
- `packages/plugin`: Source for `@aictrl/plugin`
- `packages/plugin`: Source for `@aictrl/plugin` — first-party plugin-author SDK (types + `tool()` factory). Workspace-only, marked `"private": true`, not published to npm; types are embedded in the compiled CLI binary at runtime.

### Understanding bun dev vs aictrl

Expand Down
4 changes: 2 additions & 2 deletions bun.lock

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

54 changes: 26 additions & 28 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@ import {
import { Instance } from "../project/instance"
import { LSPServer } from "../lsp/server"
import { BunProc } from "@/bun"
import { Installation } from "@/installation"
import { ConfigMarkdown } from "./markdown"
import { constants, existsSync } from "fs"
import { Bus } from "@/bus"
import { GlobalBus } from "@/bus/global"
import { Glob } from "../util/glob"
import { PackageRegistry } from "@/bun/registry"
import { proxied } from "@/util/proxied"
import { iife } from "@/util/iife"
import { Control } from "@/control"
Expand Down Expand Up @@ -247,24 +245,31 @@ export namespace Config {

export async function installDependencies(dir: string) {
const pkg = path.join(dir, "package.json")
const targetVersion = Installation.isLocal() ? "*" : Installation.VERSION

const json = await Filesystem.readJson<{ dependencies?: Record<string, string> }>(pkg).catch(() => ({
dependencies: {},
}))
json.dependencies = {
...json.dependencies,
"@aictrl/plugin": targetVersion,

// Migration: scrub legacy @aictrl/plugin* entries from user
// package.json. Older CLIs auto-added these; the npm names aren't
// ours to manage (@aictrl/plugin is an unrelated telemetry package,
// and @aictrl/plugin is internal / private). Plugin types are
// supplied by the CLI runtime, not npm.
const json = await Filesystem.readJson<{ dependencies?: Record<string, string> }>(pkg).catch(() => null)
if (json?.dependencies) {
const deps = { ...json.dependencies }
const hadLegacy = "@aictrl/plugin" in deps || "@aictrl/plugin" in deps
delete deps["@aictrl/plugin"]
delete deps["@aictrl/plugin"]
if (hadLegacy) {
json.dependencies = deps
await Filesystem.writeJson(pkg, json)
}
}
await Filesystem.writeJson(pkg, json)

const gitignore = path.join(dir, ".gitignore")
const hasGitIgnore = await Filesystem.exists(gitignore)
if (!hasGitIgnore)
await Filesystem.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))

// Install any additional dependencies defined in the package.json
// This allows local plugins and custom tools to use external packages
// Install any dependencies declared by local plugins and custom tools
// (e.g. a .aictrl/package.json that lists 'cowsay').
await BunProc.run(
[
"install",
Expand Down Expand Up @@ -304,21 +309,14 @@ export namespace Config {

const parsed = await Filesystem.readJson<{ dependencies?: Record<string, string> }>(pkg).catch(() => null)
const dependencies = parsed?.dependencies ?? {}
const depVersion = dependencies["@aictrl/plugin"]
if (!depVersion) return true

const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION
if (targetVersion === "latest") {
const isOutdated = await PackageRegistry.isOutdated("@aictrl/plugin", depVersion, dir)
if (!isOutdated) return false
log.info("Cached version is outdated, proceeding with install", {
pkg: "@aictrl/plugin",
cachedVersion: depVersion,
})
return true
}
if (depVersion === targetVersion) return false
return true

// Migration: any legacy @aictrl/plugin* entry forces a reinstall
// so installDependencies can scrub it out in the same pass.
if (dependencies["@aictrl/plugin"] || dependencies["@aictrl/plugin"]) return true

// No aictrl-managed deps; install only if user declared other deps
// but node_modules is absent (already checked above) — otherwise skip.
return false
}

function rel(item: string, patterns: string[]) {
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,36 @@ describe("getPluginName", () => {
})
})

describe("installDependencies migration", () => {
test("scrubs legacy @aictrl/plugin* keys from user package.json", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Filesystem.write(
path.join(dir, "package.json"),
JSON.stringify({
name: "existing",
dependencies: {
"@aictrl/plugin": "1.2.16",
cowsay: "^1.6.0",
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
await Config.installDependencies(tmp.path).catch(() => {})
const parsed = await Filesystem.readJson<{ dependencies: Record<string, string> }>(
path.join(tmp.path, "package.json"),
)
expect(parsed.dependencies["@aictrl/plugin"]).toBeUndefined()
expect(parsed.dependencies["cowsay"]).toBe("^1.6.0")
},
})
})
})

describe("deduplicatePlugins", () => {
test("removes duplicates keeping higher priority (later entries)", () => {
const plugins = ["global-plugin@1.0.0", "shared-plugin@1.0.0", "local-plugin@2.0.0", "shared-plugin@2.0.0"]
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@aictrl/plugin",
"version": "1.2.16",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "MIT",
"repository": {
Expand Down
Loading