aas-setup 1.0.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.
@@ -0,0 +1,62 @@
1
+ # Git Safety Guidelines
2
+
3
+ ## Safe Git Operations
4
+
5
+ AI agents should follow these principles when working with git:
6
+
7
+ ### ✅ Allowed Operations
8
+
9
+ - **Read operations**: `git status`, `git log`, `git diff`, `git show`
10
+ - **Safe commits**: `git add`, `git commit`
11
+ - **Branch management**: `git branch`, `git checkout -b`, `git switch`
12
+ - **Safe push**: `git push` (standard push without force)
13
+ - **Inspection**: `git blame`, `git ls-files`, `git rev-parse`
14
+
15
+ ### ⛔ Operations to Avoid
16
+
17
+ Avoid these dangerous git commands without explicit user approval:
18
+
19
+ - **Add all files**: `git add -A`, `git add --all` (stages all changes including untracked files; prefer `git add <specific-files>` or `git add -p` for interactive staging)
20
+ - **Force push**: `git push --force`, `git push -f` (not recommended; if absolutely required, `--force-with-lease` is safer)
21
+ - **History rewriting**: `git rebase -i`, `git filter-branch`
22
+ - **Amending pushed commits**: `git commit --amend` (only safe for local, unpushed commits)
23
+ - **Reset operations**: `git reset`, `git reset --hard`, `git reset --mixed`, `git reset --soft` (unstages files or moves HEAD; can lose work or change history)
24
+ - **Force operations**: `git checkout --force`, `git clean -f/-d`, `git branch -D`
25
+ - **Stash deletion**: `git stash drop`, `git stash clear`
26
+ - **Reference manipulation**: `git update-ref -d`, `git reflog expire`
27
+
28
+ ### Best Practices
29
+
30
+ - Always use `git --no-pager` to prevent interactive pagers in scripts
31
+ - Check repository state with `git status` before operations
32
+ - Stage files intentionally: use `git add <specific-files>` or `git add -p` for interactive staging
33
+ - Use `git diff` to verify changes before committing
34
+ - Prefer `git switch` over `git checkout` for branch switching (Git 2.23+; use `git checkout <branch>` for older versions)
35
+ - Use descriptive commit messages following conventional commits format
36
+ - Create feature branches instead of working directly on main/master
37
+ - Pull before push to avoid conflicts: `git pull origin <branch>` (or `git fetch origin && git merge origin/<branch>` for more control)
38
+
39
+ ### Error Handling
40
+
41
+ ```bash
42
+ # Check if git repository
43
+ if ! git rev-parse --git-dir > /dev/null 2>&1; then
44
+ echo "Error: Not a git repository" >&2
45
+ exit 1
46
+ fi
47
+
48
+ # Check for uncommitted changes
49
+ if ! git diff-index --quiet HEAD --; then
50
+ echo "Warning: Uncommitted changes detected" >&2
51
+ fi
52
+ ```
53
+
54
+ ## Pre-commit Checklist
55
+
56
+ - [ ] Shell scripts pass `bash -n` syntax check
57
+ - [ ] Tested with `--dry-run`
58
+ - [ ] No absolute paths in configs
59
+ - [ ] Colors and logging functions used consistently
60
+ - [ ] Error handling with `set -e` and guard clauses
61
+ - [ ] Documentation updated if workflow changed
62
+ - [ ] Git operations follow safety guidelines
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "aas-setup",
3
+ "version": "1.0.1",
4
+ "description": "Initialize Pi and OpenCode agent environments from a curated snapshot",
5
+ "type": "module",
6
+ "bin": {
7
+ "aas-setup": "./bin/aas-setup.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "configs"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --test",
16
+ "dev": "node bin/aas-setup.mjs"
17
+ },
18
+ "keywords": [
19
+ "cli",
20
+ "pi",
21
+ "opencode",
22
+ "agent-setup"
23
+ ],
24
+ "author": "kk",
25
+ "license": "MIT",
26
+ "devDependencies": {
27
+ "@biomejs/biome": "^2.5.1"
28
+ },
29
+ "dependencies": {
30
+ "@inquirer/prompts": "^7.10.1",
31
+ "chalk": "^5.3.0",
32
+ "commander": "^12.0.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ }
37
+ }
@@ -0,0 +1,22 @@
1
+ // opencode.mjs — OpenCode agent descriptor.
2
+ // Pure data; the initializer in src/commands/init.mjs loops over a list of
3
+ // these. Paths with ~ are expanded at runtime by the initializer.
4
+
5
+ /** @type {import('./types.mjs').AgentDescriptor} */
6
+ export const opencode = {
7
+ id: "opencode",
8
+ name: "OpenCode",
9
+ home: "~/.config/opencode",
10
+ configSource: "configs/opencode",
11
+ referenceDest: "~/.config/opencode/aas-setup",
12
+ install: {
13
+ kind: "curl",
14
+ url: "https://opencode.ai/install",
15
+ bin: "opencode",
16
+ },
17
+ deploy: {
18
+ overwriteFiles: ["opencode.json"],
19
+ replaceDirs: ["agent", "command"],
20
+ referenceSource: "configs/reference",
21
+ },
22
+ };
@@ -0,0 +1,22 @@
1
+ // pi.mjs — Pi agent descriptor.
2
+ // Pure data; the initializer in src/commands/init.mjs loops over a list of
3
+ // these. Paths with ~ are expanded at runtime by the initializer.
4
+
5
+ /** @type {import('./types.mjs').AgentDescriptor} */
6
+ export const pi = {
7
+ id: "pi",
8
+ name: "Pi",
9
+ home: "~/.pi/agent",
10
+ configSource: "configs/pi",
11
+ referenceDest: "~/.pi/agent/aas-setup",
12
+ install: {
13
+ kind: "npm",
14
+ pkg: "@mariozechner/pi-coding-agent",
15
+ bin: "pi",
16
+ },
17
+ deploy: {
18
+ overwriteFiles: ["settings.json", "mcp.json", "models.json", "AGENTS.md"],
19
+ replaceDirs: ["themes"],
20
+ referenceSource: "configs/reference",
21
+ },
22
+ };
@@ -0,0 +1,30 @@
1
+ // types.mjs — JSDoc type shapes for agent descriptors.
2
+ // No runtime exports; used only for type hints in editors.
3
+
4
+ /**
5
+ * @typedef {Object} InstallSpec
6
+ * @property {"npm"|"curl"} kind
7
+ * @property {string} [pkg] — npm package name (kind === "npm")
8
+ * @property {string} [url] — curl install URL (kind === "curl")
9
+ * @property {string} bin — binary name to detect on PATH
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} DeploySpec
14
+ * @property {string[]} overwriteFiles — files copied flat into home
15
+ * @property {string[]} replaceDirs — subdirs rm-rf'd then fully replaced
16
+ * @property {string} referenceSource — package-relative path to reference docs
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} AgentDescriptor
21
+ * @property {string} id
22
+ * @property {string} name
23
+ * @property {string} home — ~-prefixed agent home
24
+ * @property {string} configSource — package-relative path to agent configs
25
+ * @property {string} referenceDest — ~-prefixed destination for reference docs
26
+ * @property {InstallSpec} install
27
+ * @property {DeploySpec} deploy
28
+ */
29
+
30
+ export {};
@@ -0,0 +1,99 @@
1
+ // init.mjs — the initializer.
2
+ // Loops over the two agent descriptors and runs detect → install → (backup)
3
+ // → deploy-configs → deploy-reference. Each step honors { dryRun }.
4
+
5
+ import { homedir } from "node:os";
6
+ import { resolve } from "node:path";
7
+ import { opencode } from "../agents/opencode.mjs";
8
+ import { pi } from "../agents/pi.mjs";
9
+ import { detectBinary, runCommand } from "../lib/exec.mjs";
10
+ import { backupToTar, copyDir, copyDirFiles, copyFile, ensureDir, rmrf } from "../lib/fs.mjs";
11
+ import { log } from "../lib/log.mjs";
12
+ import { expandHome, packageRoot, pkgPath } from "../lib/paths.mjs";
13
+
14
+ const AGENTS = [pi, opencode];
15
+
16
+ export { AGENTS };
17
+
18
+ /** Directory for --backup tars: ~/ai-tools-backup-<timestamp>/ */
19
+ function backupDirFor() {
20
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
21
+ return resolve(homedir(), `ai-tools-backup-${ts}`);
22
+ }
23
+
24
+ /**
25
+ * Install step: detect the agent's binary on PATH; if missing, run the
26
+ * install command. No-op (print only) in dry-run.
27
+ */
28
+ async function installStep(agent, { dryRun }) {
29
+ const present = dryRun ? false : detectBinary(agent.install.bin);
30
+ if (present) {
31
+ if (!dryRun) log.skip(agent.name);
32
+ else log.dry(`detect ${agent.install.bin} (assume present → skip)`);
33
+ return 0;
34
+ }
35
+ // install
36
+ const cmdline = installCmdline(agent.install);
37
+ if (dryRun) {
38
+ log.dry(`detect ${agent.install.bin} → missing → install`);
39
+ }
40
+ log.install(agent.name);
41
+ return runCommand(cmdline, { dryRun });
42
+ }
43
+
44
+ function installCmdline(spec) {
45
+ if (spec.kind === "npm") return `npm install -g ${spec.pkg}`;
46
+ if (spec.kind === "curl") return `curl -fsSL ${spec.url} | bash`;
47
+ throw new Error(`unknown install kind: ${spec.kind}`);
48
+ }
49
+
50
+ /**
51
+ * Deploy agent configs: overwrite flat files, replace subdirs wholesale.
52
+ */
53
+ function deployConfigs(agent, pkgRoot, { dryRun }) {
54
+ const home = expandHome(agent.home);
55
+ const srcDir = pkgPath(pkgRoot, agent.configSource);
56
+ ensureDir(home, { dryRun });
57
+ for (const file of agent.deploy.overwriteFiles) {
58
+ copyFile(resolve(srcDir, file), resolve(home, file), { dryRun });
59
+ }
60
+ for (const dir of agent.deploy.replaceDirs) {
61
+ copyDir(resolve(srcDir, dir), resolve(home, dir), { dryRun });
62
+ }
63
+ }
64
+
65
+ function deployReference(agent, pkgRoot, { dryRun }) {
66
+ const dest = expandHome(agent.referenceDest);
67
+ const src = pkgPath(pkgRoot, agent.deploy.referenceSource);
68
+ ensureDir(dest, { dryRun });
69
+ copyDirFiles(src, dest, { dryRun });
70
+ }
71
+
72
+ /**
73
+ * Back up the agent's existing home to a tar (no-op if absent).
74
+ */
75
+ function backupAgentHome(agent, backupDir, { dryRun }) {
76
+ backupToTar(expandHome(agent.home), backupDir, { dryRun });
77
+ }
78
+
79
+ /**
80
+ * Initialize both Pi and OpenCode. Returns 0 on success, non-zero on failure.
81
+ *
82
+ * @param {{ dryRun?: boolean, backup?: boolean }} opts
83
+ * @returns {Promise<number>}
84
+ */
85
+ export async function initialize({ dryRun = false, backup = false, agents = AGENTS } = {}) {
86
+ const pkgRoot = packageRoot(import.meta.url);
87
+ const backupDir = backup ? backupDirFor() : null;
88
+ log.info(dryRun ? "Dry-run mode — no changes will be made" : "Initializing agent environments");
89
+ if (backup) log.info(`Backing up existing agent homes to ${backupDir}`);
90
+ for (const agent of agents) {
91
+ log.step(`Configure ${agent.name}`);
92
+ await installStep(agent, { dryRun });
93
+ if (backup) backupAgentHome(agent, backupDir, { dryRun });
94
+ deployConfigs(agent, pkgRoot, { dryRun });
95
+ deployReference(agent, pkgRoot, { dryRun });
96
+ }
97
+ log.success("Setup complete");
98
+ return 0;
99
+ }
@@ -0,0 +1,52 @@
1
+ // select.mjs — resolve which agents to configure.
2
+ //
3
+ // Resolution order:
4
+ // 1. positional args (aas-setup pi opencode) → validate ids, use exactly those
5
+ // 2. non-TTY (CI / piped) → all agents, no prompt
6
+ // 3. TTY → interactive multi-select,
7
+ // both pre-checked by default
8
+ //
9
+ // Exits non-zero on unknown agent ids.
10
+
11
+ import { checkbox } from "@inquirer/prompts";
12
+ import { log } from "../lib/log.mjs";
13
+ import { AGENTS } from "./init.mjs";
14
+
15
+ const VALID_IDS = new Set(AGENTS.map((a) => a.id));
16
+
17
+ export async function selectAgents(positional, { tty = false } = {}) {
18
+ // 1. explicit positional args
19
+ if (positional.length > 0) {
20
+ const invalid = positional.filter((id) => !VALID_IDS.has(id));
21
+ if (invalid.length > 0) {
22
+ log.error(`Unknown agent(s): ${invalid.join(", ")}`);
23
+ log.info(`Valid ids: ${AGENTS.map((a) => a.id).join(", ")}`);
24
+ process.exit(1);
25
+ }
26
+ return positional;
27
+ }
28
+
29
+ // 2. non-interactive: all agents
30
+ if (!tty) {
31
+ log.info("Non-interactive mode — configuring all agents");
32
+ return AGENTS.map((a) => a.id);
33
+ }
34
+
35
+ // 3. interactive multi-select
36
+ const selected = await checkbox({
37
+ message: "Select agents to configure:",
38
+ default: true,
39
+ choices: AGENTS.map((a) => ({
40
+ name: a.name,
41
+ value: a.id,
42
+ checked: true,
43
+ })),
44
+ });
45
+
46
+ if (selected.length === 0) {
47
+ log.warning("No agents selected — nothing to do");
48
+ process.exit(0);
49
+ }
50
+
51
+ return selected;
52
+ }
@@ -0,0 +1,35 @@
1
+ // exec.mjs — exec + detect helpers that honor --dry-run.
2
+ //
3
+ // runCommand takes a single shell command string. In dry-run mode it prints
4
+ // "[dry] <cmd>" instead of spawning. In real mode it spawns with shell:true
5
+ // and inherited stdio, returning the exit code.
6
+
7
+ import { spawn, spawnSync } from "node:child_process";
8
+
9
+ /**
10
+ * Run a shell command. If dryRun is true, only print the command.
11
+ * Returns 0 on success, non-zero on failure. Resolves a Promise.
12
+ */
13
+ export function runCommand(cmdline, { dryRun = false, cwd } = {}) {
14
+ if (dryRun) {
15
+ console.log(`[dry] ${cmdline}`);
16
+ return Promise.resolve(0);
17
+ }
18
+ return new Promise((resolve) => {
19
+ const child = spawn(cmdline, { cwd, stdio: "inherit", shell: true });
20
+ child.on("close", (code) => resolve(code ?? 1));
21
+ child.on("error", () => resolve(1));
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Detect whether a binary is on PATH. Synchronous — used by the detect step
27
+ * to decide install-vs-skip. Returns boolean.
28
+ */
29
+ export function detectBinary(name) {
30
+ const result = spawnSync("command", ["-v", name], {
31
+ shell: true,
32
+ stdio: "ignore",
33
+ });
34
+ return result.status === 0;
35
+ }
package/src/lib/fs.mjs ADDED
@@ -0,0 +1,107 @@
1
+ // fs.mjs — filesystem helpers for deploy.
2
+ // All functions take an optional { dryRun } option: when true, print the
3
+ // planned action with full paths and do NOT touch the filesystem.
4
+
5
+ import { execFileSync } from "node:child_process";
6
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { basename, dirname, resolve } from "node:path";
9
+ import { log } from "./log.mjs";
10
+
11
+ /** Strip the leading home directory from an absolute path for nicer tar names. */
12
+ function stripHome(p) {
13
+ const h = homedir();
14
+ return p.startsWith(h) ? p.slice(h.length).replace(/^\/+/, "") : p;
15
+ }
16
+
17
+ /**
18
+ * Ensure a directory exists (recursive). Prints in dry-run.
19
+ */
20
+ export function ensureDir(dir, { dryRun = false } = {}) {
21
+ if (dryRun) {
22
+ log.dry(`mkdir -p ${dir}`);
23
+ return;
24
+ }
25
+ if (!existsSync(dir)) {
26
+ mkdirSync(dir, { recursive: true });
27
+ log.info(`mkdir ${dir}`);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Remove a directory recursively. No-op if it doesn't exist. Prints in dry-run.
33
+ */
34
+ export function rmrf(dir, { dryRun = false } = {}) {
35
+ if (dryRun) {
36
+ if (existsSync(dir)) log.dry(`rm -rf ${dir}`);
37
+ return;
38
+ }
39
+ if (existsSync(dir)) {
40
+ rmSync(dir, { recursive: true, force: true });
41
+ log.info(`rm -rf ${dir}`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Copy a single file, creating parent dirs as needed. Prints in dry-run.
47
+ */
48
+ export function copyFile(src, dest, { dryRun = false } = {}) {
49
+ if (dryRun) {
50
+ log.dry(`cp ${src} → ${dest}`);
51
+ return;
52
+ }
53
+ ensureDir(dirname(dest), { dryRun: false });
54
+ cpSync(src, dest, { force: true });
55
+ log.deploy(dest);
56
+ }
57
+
58
+ /**
59
+ * Copy a directory recursively, fully replacing the destination.
60
+ * Calls rmrf(dest) then copies src → dest. Prints in dry-run.
61
+ */
62
+ export function copyDir(src, dest, { dryRun = false } = {}) {
63
+ if (dryRun) {
64
+ log.dry(`rm -rf ${dest} && cp -r ${src} ${dest}`);
65
+ return;
66
+ }
67
+ rmrf(dest, { dryRun: false });
68
+ ensureDir(dirname(dest), { dryRun: false });
69
+ cpSync(src, dest, { recursive: true, force: true });
70
+ log.deploy(dest);
71
+ }
72
+
73
+ /**
74
+ * Copy every file in sourceDir (non-recursively) into destDir.
75
+ * Used to deploy reference docs (flat file set). Prints in dry-run.
76
+ */
77
+ export function copyDirFiles(sourceDir, destDir, { dryRun = false } = {}) {
78
+ if (!existsSync(sourceDir)) return;
79
+ const entries = readdirSync(sourceDir);
80
+ for (const entry of entries) {
81
+ const src = resolve(sourceDir, entry);
82
+ const stat = statSync(src);
83
+ if (stat.isFile()) {
84
+ copyFile(src, resolve(destDir, entry), { dryRun });
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Back up a directory tree to a tar file under backupDir.
91
+ * Skips silently if the source does not exist. Prints in dry-run.
92
+ * Returns the created tar path (or null if skipped/no-op).
93
+ */
94
+ export function backupToTar(src, backupDir, { dryRun = false } = {}) {
95
+ if (!existsSync(src)) return null;
96
+ const base = basename(stripHome(src));
97
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
98
+ const tar = resolve(backupDir, `${base}-${ts}.tar.gz`);
99
+ if (dryRun) {
100
+ log.dry(`backup ${src} → ${tar}`);
101
+ return tar;
102
+ }
103
+ ensureDir(backupDir, { dryRun: false });
104
+ execFileSync("tar", ["-czf", tar, "-C", dirname(src), base], { stdio: "ignore" });
105
+ log.info(`backup ${src} → ${tar}`);
106
+ return tar;
107
+ }
@@ -0,0 +1,39 @@
1
+ // log.mjs — structured stdout logger built on chalk.
2
+ // Every action prints a full resolved path so the user can see exactly what
3
+ // the CLI did.
4
+
5
+ import chalk from "chalk";
6
+
7
+ function fullPath(p) {
8
+ return typeof p === "string" ? p : String(p);
9
+ }
10
+
11
+ export const log = {
12
+ info(msg) {
13
+ console.log(`${chalk.cyan("ℹ")} ${msg}`);
14
+ },
15
+ success(msg) {
16
+ console.log(`${chalk.green("✓")} ${msg}`);
17
+ },
18
+ warning(msg) {
19
+ console.log(`${chalk.yellow("⚠")} ${msg}`);
20
+ },
21
+ error(msg) {
22
+ console.error(`${chalk.red("✗")} ${msg}`);
23
+ },
24
+ dry(msg) {
25
+ console.log(`${chalk.magenta("[dry]")} ${msg}`);
26
+ },
27
+ step(msg) {
28
+ console.log(`${chalk.dim("─")} ${msg}`);
29
+ },
30
+ install(name) {
31
+ console.log(`${chalk.cyan("ℹ")} Installing ${name}...`);
32
+ },
33
+ skip(name) {
34
+ console.log(`${chalk.dim("•")} ${name} already installed, skipping`);
35
+ },
36
+ deploy(dest) {
37
+ console.log(`${chalk.green("✓")} Deployed → ${fullPath(dest)}`);
38
+ },
39
+ };
@@ -0,0 +1,47 @@
1
+ // paths.mjs — resolve package root and agent homes.
2
+ // Resolves from import.meta.url (NOT process.cwd()) so `npx aas-setup` works
3
+ // regardless of the user's current directory.
4
+
5
+ import { homedir } from "node:os";
6
+ import { dirname, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ /**
10
+ * Resolve the package root (the directory containing bin/, src/, configs/).
11
+ * Pass import.meta.url from the calling module.
12
+ */
13
+ export function packageRoot(importMetaUrl) {
14
+ const here = dirname(fileURLToPath(importMetaUrl));
15
+ // src/lib/paths.mjs → up two = package root
16
+ return resolve(here, "..", "..");
17
+ }
18
+
19
+ /**
20
+ * Resolve a path inside the package root. Segments are package-relative
21
+ * (e.g. "configs", "pi", "settings.json"). Descriptors already carry the
22
+ * "configs/<agent>" prefix, so this does NOT add a magic "configs/".
23
+ */
24
+ export function pkgPath(pkgRoot, ...segments) {
25
+ return resolve(pkgRoot, ...segments);
26
+ }
27
+
28
+ /**
29
+ * Expand a leading ~ to the user's home directory.
30
+ */
31
+ export function expandHome(p) {
32
+ if (!p) return p;
33
+ if (p === "~") return homedir();
34
+ if (p.startsWith("~/")) return resolve(homedir(), p.slice(2));
35
+ return p;
36
+ }
37
+
38
+ /**
39
+ * The two agent homes, as absolute paths.
40
+ */
41
+ export function agentHomes() {
42
+ const home = homedir();
43
+ return {
44
+ pi: resolve(home, ".pi", "agent"),
45
+ opencode: resolve(home, ".config", "opencode"),
46
+ };
47
+ }