code-ai-installer 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.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # code-ai installer
2
+
3
+ CLI for installing your `agents/` and `.agents/skills/` assets into AI-specific layouts.
4
+
5
+ ## Supported targets
6
+ - `vscode-copilot`
7
+ - `gpt-codex`
8
+ - `claude`
9
+ - `qwen-3.5`
10
+ - `google-antugravity`
11
+
12
+ ## Install
13
+ ```bash
14
+ npm install
15
+ npm run build
16
+ ```
17
+
18
+ ## Global install
19
+ ```bash
20
+ # from npm registry
21
+ npm install -g code-ai-installer
22
+ # or
23
+ pnpm add -g code-ai-installer
24
+ # or
25
+ yarn global add code-ai-installer
26
+ # or
27
+ bun add -g code-ai-installer
28
+
29
+ # verify binary in system terminal
30
+ code-ai --help
31
+ ```
32
+
33
+ ```bash
34
+ # install globally from local repository
35
+ npm install -g .
36
+ code-ai --help
37
+ ```
38
+
39
+ ## Usage
40
+ ```bash
41
+ # interactive wizard (recommended)
42
+ code-ai
43
+
44
+ # list targets
45
+ code-ai targets
46
+
47
+ # list available agents and skills from current repository
48
+ code-ai list
49
+
50
+ # health checks
51
+ code-ai doctor --target claude
52
+
53
+ # dry-run install (default)
54
+ code-ai install --target claude --agents conductor,reviewer --skills board,security_review
55
+
56
+ # apply install with overwrite
57
+ code-ai install --target claude --agents all --skills all --overwrite --apply
58
+
59
+ # strict mode: require explicit target hints in agent/skill files
60
+ code-ai install --target claude --agents all --skills all --strict-hints --apply
61
+
62
+ # uninstall preview
63
+ code-ai uninstall --target claude
64
+
65
+ # uninstall apply
66
+ code-ai uninstall --target claude --apply
67
+ ```
68
+
69
+ ## What gets generated
70
+ - Root orchestration mirror: `AGENTS.md`
71
+ - Target instruction file:
72
+ - Copilot: `.github/copilot-instructions.md`
73
+ - GPT Codex: `CODEX.md`
74
+ - Claude: `CLAUDE.md`
75
+ - Qwen: `QWEN.md`
76
+ - Google Antugravity: `GEMINI.md`
77
+ - Agent files copied to target-specific agents directory.
78
+ - Skill files copied to target-specific skills directory.
79
+ - Markdown content is normalized per target (model-specific hint comments like `codex:` are transformed).
80
+ - `codex reasoning` is mapped to target-native hints (`claude thinking`, `qwen reasoning_effort`, `gemini reasoning`).
81
+
82
+ ## Safety model
83
+ - Default mode is `dry-run`.
84
+ - Backup is created before writes under `.code-ai-installer/backups/<target>/<timestamp>/`.
85
+ - State is tracked in `.code-ai-installer/state/<target>.json` for uninstall.
86
+ - Rollback restores previous files on install failure.
87
+ - Optional strict policy: `--strict-hints` fails install if selected agent/skill files don't contain explicit target-native hint markers.
88
+
89
+ ## Notes
90
+ - Target aliases are accepted: `copilot`, `codex`, `claude`, `qwen`, `google`, `antigravity`.
91
+ - If your AI tool requires a custom location, pass `--destination <path>`.
@@ -0,0 +1,27 @@
1
+ import type { SourceCatalog } from "./types.js";
2
+ /**
3
+ * Builds source catalog from repository folders and validates required paths.
4
+ * @param projectDir Absolute path to repository root.
5
+ * @returns Source catalog including orchestrator, agents, and skills.
6
+ */
7
+ export declare function loadSourceCatalog(projectDir: string): Promise<SourceCatalog>;
8
+ /**
9
+ * Returns available role names from catalog map.
10
+ * @param catalog Source catalog.
11
+ * @returns Sorted agent names.
12
+ */
13
+ export declare function listAgentNames(catalog: SourceCatalog): string[];
14
+ /**
15
+ * Returns available skill names from catalog map.
16
+ * @param catalog Source catalog.
17
+ * @returns Sorted skill names.
18
+ */
19
+ export declare function listSkillNames(catalog: SourceCatalog): string[];
20
+ /**
21
+ * Resolves selected names, expanding "all" and validating values.
22
+ * @param selected Comma-separated user selection string.
23
+ * @param available List of available names.
24
+ * @param entityLabel Human-readable entity label for errors.
25
+ * @returns Validated selection array.
26
+ */
27
+ export declare function resolveSelection(selected: string, available: string[], entityLabel: string): string[];
@@ -0,0 +1,134 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { z } from "zod";
4
+ const fileNameSchema = z.string().min(1);
5
+ /**
6
+ * Builds source catalog from repository folders and validates required paths.
7
+ * @param projectDir Absolute path to repository root.
8
+ * @returns Source catalog including orchestrator, agents, and skills.
9
+ */
10
+ export async function loadSourceCatalog(projectDir) {
11
+ const orchestratorPath = path.join(projectDir, "AGENTS.md");
12
+ const agentsDir = path.join(projectDir, "agents");
13
+ const legacySkillsDir = path.join(projectDir, ".agents", "skills");
14
+ const flatSkillsDir = path.join(projectDir, ".agents");
15
+ if (!(await fs.pathExists(orchestratorPath))) {
16
+ throw new Error(`Missing AGENTS.md at ${orchestratorPath}`);
17
+ }
18
+ if (!(await fs.pathExists(agentsDir))) {
19
+ throw new Error(`Missing agents directory at ${agentsDir}`);
20
+ }
21
+ const hasLegacySkillsDir = await fs.pathExists(legacySkillsDir);
22
+ const hasFlatSkillsDir = await fs.pathExists(flatSkillsDir);
23
+ if (!hasLegacySkillsDir && !hasFlatSkillsDir) {
24
+ throw new Error(`Missing skills directory. Checked ${legacySkillsDir} and ${flatSkillsDir}`);
25
+ }
26
+ const agentFiles = await mapAgentFiles(agentsDir);
27
+ const skillFiles = {};
28
+ if (hasLegacySkillsDir) {
29
+ Object.assign(skillFiles, await mapSkillFilesFromNestedRoot(legacySkillsDir));
30
+ }
31
+ if (hasFlatSkillsDir) {
32
+ Object.assign(skillFiles, await mapSkillFilesFromFlatRoot(flatSkillsDir));
33
+ }
34
+ return {
35
+ rootDir: projectDir,
36
+ orchestratorPath,
37
+ agentFiles,
38
+ skillFiles,
39
+ };
40
+ }
41
+ /**
42
+ * Returns available role names from catalog map.
43
+ * @param catalog Source catalog.
44
+ * @returns Sorted agent names.
45
+ */
46
+ export function listAgentNames(catalog) {
47
+ return Object.keys(catalog.agentFiles).sort((a, b) => a.localeCompare(b));
48
+ }
49
+ /**
50
+ * Returns available skill names from catalog map.
51
+ * @param catalog Source catalog.
52
+ * @returns Sorted skill names.
53
+ */
54
+ export function listSkillNames(catalog) {
55
+ return Object.keys(catalog.skillFiles).sort((a, b) => a.localeCompare(b));
56
+ }
57
+ /**
58
+ * Resolves selected names, expanding "all" and validating values.
59
+ * @param selected Comma-separated user selection string.
60
+ * @param available List of available names.
61
+ * @param entityLabel Human-readable entity label for errors.
62
+ * @returns Validated selection array.
63
+ */
64
+ export function resolveSelection(selected, available, entityLabel) {
65
+ const normalized = selected
66
+ .split(",")
67
+ .map((name) => name.trim())
68
+ .filter(Boolean);
69
+ if (normalized.length === 0 || normalized.includes("all")) {
70
+ return [...available];
71
+ }
72
+ const missing = normalized.filter((name) => !available.includes(name));
73
+ if (missing.length > 0) {
74
+ throw new Error(`Unknown ${entityLabel}: ${missing.join(", ")}`);
75
+ }
76
+ return normalized;
77
+ }
78
+ /**
79
+ * Maps agent markdown files from agents directory.
80
+ * @param agentsDir Absolute agents directory path.
81
+ * @returns Agent name to file path mapping.
82
+ */
83
+ async function mapAgentFiles(agentsDir) {
84
+ const entries = await fs.readdir(agentsDir);
85
+ const result = {};
86
+ for (const entry of entries) {
87
+ fileNameSchema.parse(entry);
88
+ if (!entry.endsWith(".md")) {
89
+ continue;
90
+ }
91
+ const key = entry.replace(/\.md$/i, "");
92
+ result[key] = path.join(agentsDir, entry);
93
+ }
94
+ return result;
95
+ }
96
+ /**
97
+ * Maps skills from nested skill directories ending with SKILL.md.
98
+ * @param skillsDir Absolute skills directory path.
99
+ * @returns Skill name to SKILL.md path mapping.
100
+ */
101
+ async function mapSkillFilesFromNestedRoot(skillsDir) {
102
+ const entries = await fs.readdir(skillsDir);
103
+ const result = {};
104
+ for (const entry of entries) {
105
+ fileNameSchema.parse(entry);
106
+ const skillFile = path.join(skillsDir, entry, "SKILL.md");
107
+ if (!(await fs.pathExists(skillFile))) {
108
+ continue;
109
+ }
110
+ result[entry] = skillFile;
111
+ }
112
+ return result;
113
+ }
114
+ /**
115
+ * Maps skills from nested .agents folders that contain SKILL.md.
116
+ * @param agentsRoot Absolute .agents directory path.
117
+ * @returns Skill name to SKILL.md path mapping.
118
+ */
119
+ async function mapSkillFilesFromFlatRoot(agentsRoot) {
120
+ const entries = await fs.readdir(agentsRoot);
121
+ const result = {};
122
+ for (const entry of entries) {
123
+ fileNameSchema.parse(entry);
124
+ if (entry === "skills") {
125
+ continue;
126
+ }
127
+ const skillFile = path.join(agentsRoot, entry, "SKILL.md");
128
+ if (!(await fs.pathExists(skillFile))) {
129
+ continue;
130
+ }
131
+ result[entry] = skillFile;
132
+ }
133
+ return result;
134
+ }
@@ -0,0 +1,16 @@
1
+ import type { TargetId } from "./types.js";
2
+ /**
3
+ * Transforms markdown content for selected AI target.
4
+ * @param input Original file content.
5
+ * @param target Target AI identifier.
6
+ * @param assetType Installed asset type.
7
+ * @returns Target-normalized markdown content.
8
+ */
9
+ export declare function transformContentForTarget(input: string, target: TargetId, assetType: "orchestrator" | "agent" | "skill"): string;
10
+ /**
11
+ * Returns whether markdown contains explicit hint for the selected target model.
12
+ * @param input Original markdown content.
13
+ * @param target Target AI identifier.
14
+ * @returns True when a native target hint is present.
15
+ */
16
+ export declare function hasExplicitTargetHint(input: string, target: TargetId): boolean;
@@ -0,0 +1,195 @@
1
+ const hintRegex = /<!--\s*(codex|claude|qwen|gemini)\s*:\s*([\s\S]*?)-->\s*\n?/gi;
2
+ /**
3
+ * Transforms markdown content for selected AI target.
4
+ * @param input Original file content.
5
+ * @param target Target AI identifier.
6
+ * @param assetType Installed asset type.
7
+ * @returns Target-normalized markdown content.
8
+ */
9
+ export function transformContentForTarget(input, target, assetType) {
10
+ const normalizedInput = stripBom(input).replace(/\r\n/g, "\n");
11
+ const parsedHints = [];
12
+ const foundHints = new Set();
13
+ const cleaned = normalizedInput.replace(hintRegex, (_match, model, payload) => {
14
+ const normalizedModel = String(model).toLowerCase();
15
+ foundHints.add(normalizedModel);
16
+ parsedHints.push({ model: normalizedModel, payload: String(payload).trim() });
17
+ return "";
18
+ });
19
+ const marker = buildMarker(target, assetType, [...foundHints]);
20
+ const mappedHint = buildMappedHint(target, parsedHints);
21
+ const output = cleaned.trimStart();
22
+ return `${marker}${mappedHint}${output.endsWith("\n") ? output : `${output}\n`}`;
23
+ }
24
+ /**
25
+ * Returns whether markdown contains explicit hint for the selected target model.
26
+ * @param input Original markdown content.
27
+ * @param target Target AI identifier.
28
+ * @returns True when a native target hint is present.
29
+ */
30
+ export function hasExplicitTargetHint(input, target) {
31
+ const content = stripBom(input).replace(/\r\n/g, "\n");
32
+ const targetModel = targetToModel(target);
33
+ const hints = [...content.matchAll(/<!--\s*(codex|claude|qwen|gemini)\s*:\s*[\s\S]*?-->/gi)];
34
+ return hints.some((match) => String(match[1]).toLowerCase() === targetModel);
35
+ }
36
+ /**
37
+ * Returns content without UTF-8 BOM prefix.
38
+ * @param value Raw content.
39
+ * @returns BOM-free content.
40
+ */
41
+ function stripBom(value) {
42
+ return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value;
43
+ }
44
+ /**
45
+ * Builds target marker block for transformed files.
46
+ * @param target Target AI identifier.
47
+ * @param assetType Installed asset type.
48
+ * @param foundHints Removed model-specific hints.
49
+ * @returns Marker prefix.
50
+ */
51
+ function buildMarker(target, assetType, foundHints) {
52
+ const hints = foundHints.length > 0 ? foundHints.join(",") : "none";
53
+ return `<!-- code-ai: target=${target}; asset=${assetType}; normalized_hints=${hints} -->\n`;
54
+ }
55
+ /**
56
+ * Builds target model hint from available source hints.
57
+ * @param target Target AI identifier.
58
+ * @param hints Parsed source hints.
59
+ * @returns Target-specific hint marker or empty string.
60
+ */
61
+ function buildMappedHint(target, hints) {
62
+ const targetModel = targetToModel(target);
63
+ const native = hints.find((hint) => hint.model === targetModel);
64
+ if (native) {
65
+ return `<!-- ${targetModel}: ${native.payload} -->\n`;
66
+ }
67
+ const codex = hints.find((hint) => hint.model === "codex");
68
+ if (!codex) {
69
+ return "";
70
+ }
71
+ const mappedPayload = mapCodexPayload(codex.payload, targetModel);
72
+ return `<!-- ${targetModel}: ${mappedPayload} -->\n`;
73
+ }
74
+ /**
75
+ * Maps target id to hint model label.
76
+ * @param target Target AI identifier.
77
+ * @returns Target model name used in markdown hints.
78
+ */
79
+ function targetToModel(target) {
80
+ if (target === "claude") {
81
+ return "claude";
82
+ }
83
+ if (target === "qwen-3.5") {
84
+ return "qwen";
85
+ }
86
+ if (target === "google-antugravity") {
87
+ return "gemini";
88
+ }
89
+ return "codex";
90
+ }
91
+ /**
92
+ * Converts codex hint payload to a target-model hint payload.
93
+ * @param payload Codex payload (for example: reasoning=high; note="..." ).
94
+ * @param targetModel Destination model.
95
+ * @returns Converted payload string.
96
+ */
97
+ function mapCodexPayload(payload, targetModel) {
98
+ const reasoning = extractValue(payload, "reasoning");
99
+ const note = extractValue(payload, "note");
100
+ const normalizedReasoning = normalizeReasoning(reasoning);
101
+ if (targetModel === "claude") {
102
+ const thinking = mapReasoningToClaude(normalizedReasoning);
103
+ return withNote(`thinking=${thinking}`, note);
104
+ }
105
+ if (targetModel === "qwen") {
106
+ const effort = mapReasoningToQwen(normalizedReasoning);
107
+ return withNote(`reasoning_effort=${effort}`, note);
108
+ }
109
+ if (targetModel === "gemini") {
110
+ const effort = mapReasoningToGemini(normalizedReasoning);
111
+ return withNote(`reasoning=${effort}`, note);
112
+ }
113
+ return withNote(`reasoning=${normalizedReasoning}`, note);
114
+ }
115
+ /**
116
+ * Reads one key value from semicolon-separated payload.
117
+ * @param payload Input payload string.
118
+ * @param key Value key name.
119
+ * @returns Extracted value without quotes.
120
+ */
121
+ function extractValue(payload, key) {
122
+ const pattern = new RegExp(`${key}\\s*=\\s*("([^"]*)"|[^;]+)`, "i");
123
+ const match = payload.match(pattern);
124
+ if (!match) {
125
+ return "";
126
+ }
127
+ const raw = match[2] ?? match[1] ?? "";
128
+ return raw.trim();
129
+ }
130
+ /**
131
+ * Normalizes codex reasoning aliases into low/medium/high/extra_high.
132
+ * @param value Raw reasoning value.
133
+ * @returns Normalized reasoning value.
134
+ */
135
+ function normalizeReasoning(value) {
136
+ const normalized = value.trim().toLowerCase();
137
+ if (normalized === "xhigh" || normalized === "very_high" || normalized === "extra-high") {
138
+ return "extra_high";
139
+ }
140
+ if (normalized === "extra_high") {
141
+ return "extra_high";
142
+ }
143
+ if (normalized === "high") {
144
+ return "high";
145
+ }
146
+ if (normalized === "low") {
147
+ return "low";
148
+ }
149
+ return "medium";
150
+ }
151
+ /**
152
+ * Maps normalized reasoning to Claude thinking levels.
153
+ * @param reasoning Normalized codex reasoning.
154
+ * @returns Claude thinking level.
155
+ */
156
+ function mapReasoningToClaude(reasoning) {
157
+ if (reasoning === "extra_high") {
158
+ return "max";
159
+ }
160
+ return reasoning;
161
+ }
162
+ /**
163
+ * Maps normalized reasoning to Qwen effort levels.
164
+ * @param reasoning Normalized codex reasoning.
165
+ * @returns Qwen reasoning effort.
166
+ */
167
+ function mapReasoningToQwen(reasoning) {
168
+ if (reasoning === "extra_high") {
169
+ return "high";
170
+ }
171
+ return reasoning;
172
+ }
173
+ /**
174
+ * Maps normalized reasoning to Gemini reasoning levels.
175
+ * @param reasoning Normalized codex reasoning.
176
+ * @returns Gemini reasoning level.
177
+ */
178
+ function mapReasoningToGemini(reasoning) {
179
+ if (reasoning === "extra_high") {
180
+ return "high";
181
+ }
182
+ return reasoning;
183
+ }
184
+ /**
185
+ * Appends optional note to mapped hint payload.
186
+ * @param base Base payload.
187
+ * @param note Optional note value.
188
+ * @returns Payload with optional quoted note.
189
+ */
190
+ function withNote(base, note) {
191
+ if (!note) {
192
+ return base;
193
+ }
194
+ return `${base}; note="${note}"`;
195
+ }
@@ -0,0 +1,13 @@
1
+ import type { TargetId } from "./types.js";
2
+ /**
3
+ * Runs local diagnostics for source and destination readiness.
4
+ * @param projectDir Repository root.
5
+ * @param destinationDir Install destination.
6
+ * @param target Target id.
7
+ * @returns Diagnostic messages categorized by severity.
8
+ */
9
+ export declare function runDoctor(projectDir: string, destinationDir: string, target: TargetId): Promise<{
10
+ errors: string[];
11
+ warnings: string[];
12
+ info: string[];
13
+ }>;
package/dist/doctor.js ADDED
@@ -0,0 +1,37 @@
1
+ import fs from "fs-extra";
2
+ import { getPlatformAdapter } from "./platforms/adapters.js";
3
+ import { loadSourceCatalog } from "./catalog.js";
4
+ /**
5
+ * Runs local diagnostics for source and destination readiness.
6
+ * @param projectDir Repository root.
7
+ * @param destinationDir Install destination.
8
+ * @param target Target id.
9
+ * @returns Diagnostic messages categorized by severity.
10
+ */
11
+ export async function runDoctor(projectDir, destinationDir, target) {
12
+ const errors = [];
13
+ const warnings = [];
14
+ const info = [];
15
+ try {
16
+ const catalog = await loadSourceCatalog(projectDir);
17
+ info.push(`Found ${Object.keys(catalog.agentFiles).length} agents.`);
18
+ info.push(`Found ${Object.keys(catalog.skillFiles).length} skills.`);
19
+ }
20
+ catch (error) {
21
+ errors.push(error.message);
22
+ }
23
+ const adapter = getPlatformAdapter(target);
24
+ warnings.push(...adapter.validateDestination(destinationDir));
25
+ try {
26
+ await fs.ensureDir(destinationDir);
27
+ const probeFile = `${destinationDir}/.code-ai-installer/.write-probe`;
28
+ await fs.ensureDir(`${destinationDir}/.code-ai-installer`);
29
+ await fs.writeFile(probeFile, "ok", "utf8");
30
+ await fs.remove(probeFile);
31
+ info.push("Destination is writable.");
32
+ }
33
+ catch (error) {
34
+ errors.push(`Destination is not writable: ${error.message}`);
35
+ }
36
+ return { errors, warnings, info };
37
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};