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/dist/index.js ADDED
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import prompts from "prompts";
4
+ import { Command } from "commander";
5
+ import { loadSourceCatalog, listAgentNames, listSkillNames, resolveSelection } from "./catalog.js";
6
+ import { runDoctor } from "./doctor.js";
7
+ import { runInstall, runUninstall } from "./installer.js";
8
+ import { error, info, success, warn } from "./logger.js";
9
+ import { getPlatformAdapters } from "./platforms/adapters.js";
10
+ const program = new Command();
11
+ program
12
+ .name("code-ai")
13
+ .description("Install code-ai agents and skills for AI coding assistants")
14
+ .version("1.0.0");
15
+ program
16
+ .command("targets")
17
+ .description("List supported AI targets")
18
+ .action(() => {
19
+ const adapters = getPlatformAdapters();
20
+ for (const adapter of adapters) {
21
+ info(`${adapter.id} - ${adapter.label}`);
22
+ info(` ${adapter.description}`);
23
+ }
24
+ });
25
+ program
26
+ .command("list")
27
+ .description("List available agents and skills from current repository")
28
+ .option("--project-dir <path>", "Project root path", process.cwd())
29
+ .action(async (options) => {
30
+ try {
31
+ const projectDir = path.resolve(options.projectDir);
32
+ const catalog = await loadSourceCatalog(projectDir);
33
+ const agents = listAgentNames(catalog);
34
+ const skills = listSkillNames(catalog);
35
+ info(`Agents (${agents.length}): ${agents.join(", ")}`);
36
+ info(`Skills (${skills.length}): ${skills.join(", ")}`);
37
+ }
38
+ catch (err) {
39
+ error(err.message);
40
+ process.exitCode = 1;
41
+ }
42
+ });
43
+ program
44
+ .command("doctor")
45
+ .description("Check source and destination health before install")
46
+ .requiredOption("--target <id>", "Target AI id")
47
+ .option("--project-dir <path>", "Project root path", process.cwd())
48
+ .option("--destination <path>", "Destination root (default: project root)")
49
+ .action(async (options) => {
50
+ try {
51
+ const target = normalizeTarget(options.target);
52
+ const projectDir = path.resolve(options.projectDir);
53
+ const destinationDir = path.resolve(options.destination ?? projectDir);
54
+ const report = await runDoctor(projectDir, destinationDir, target);
55
+ for (const line of report.info) {
56
+ info(line);
57
+ }
58
+ for (const line of report.warnings) {
59
+ warn(line);
60
+ }
61
+ for (const line of report.errors) {
62
+ error(line);
63
+ }
64
+ if (report.errors.length > 0) {
65
+ process.exitCode = 1;
66
+ }
67
+ else {
68
+ success("Doctor checks passed.");
69
+ }
70
+ }
71
+ catch (err) {
72
+ error(err.message);
73
+ process.exitCode = 1;
74
+ }
75
+ });
76
+ program
77
+ .command("install")
78
+ .description("Install selected agents and skills for target AI")
79
+ .requiredOption("--target <id>", "Target AI id")
80
+ .option("--project-dir <path>", "Project root path", process.cwd())
81
+ .option("--destination <path>", "Destination root (default: project root)")
82
+ .option("--agents <list>", "Comma list of agents or 'all'", "all")
83
+ .option("--skills <list>", "Comma list of skills or 'all'", "all")
84
+ .option("--overwrite", "Overwrite existing files", false)
85
+ .option("--strict-hints", "Require explicit target-native hints in agent/skill files", false)
86
+ .option("--apply", "Execute file writes (default is dry-run)", false)
87
+ .action(async (options) => {
88
+ try {
89
+ const target = normalizeTarget(options.target);
90
+ const projectDir = path.resolve(options.projectDir);
91
+ const destinationDir = path.resolve(options.destination ?? projectDir);
92
+ const catalog = await loadSourceCatalog(projectDir);
93
+ const selectedAgents = resolveSelection(options.agents, listAgentNames(catalog), "agents");
94
+ const selectedSkills = resolveSelection(options.skills, listSkillNames(catalog), "skills");
95
+ const dryRun = !options.apply;
96
+ const overwriteMode = options.overwrite ? "overwrite" : "skip";
97
+ if (dryRun) {
98
+ warn("Running in dry-run mode. Use --apply to write files.");
99
+ }
100
+ const { state, result } = await runInstall({
101
+ target,
102
+ projectDir,
103
+ destinationDir,
104
+ selectedAgents,
105
+ selectedSkills,
106
+ dryRun,
107
+ overwriteMode,
108
+ strictHints: options.strictHints,
109
+ });
110
+ info(`Target: ${state.target}`);
111
+ info(`Destination: ${state.destinationDir}`);
112
+ info(`Files planned: ${result.plannedFiles.length}`);
113
+ if (dryRun) {
114
+ info(`Files would write: ${result.writtenFiles.length}`);
115
+ }
116
+ else {
117
+ info(`Files written: ${result.writtenFiles.length}`);
118
+ }
119
+ info(`Files skipped: ${result.skippedFiles.length}`);
120
+ if (result.backupDir) {
121
+ info(`Backup: ${result.backupDir}`);
122
+ }
123
+ if (dryRun) {
124
+ success("Dry-run completed.");
125
+ }
126
+ else {
127
+ success("Install completed.");
128
+ }
129
+ }
130
+ catch (err) {
131
+ error(err.message);
132
+ process.exitCode = 1;
133
+ }
134
+ });
135
+ program
136
+ .command("uninstall")
137
+ .description("Uninstall previously installed files for target")
138
+ .requiredOption("--target <id>", "Target AI id")
139
+ .option("--destination <path>", "Destination root", process.cwd())
140
+ .option("--apply", "Execute delete operations (default is dry-run)", false)
141
+ .action(async (options) => {
142
+ try {
143
+ const target = normalizeTarget(options.target);
144
+ const destinationDir = path.resolve(options.destination);
145
+ const dryRun = !options.apply;
146
+ if (dryRun) {
147
+ warn("Running in dry-run mode. Use --apply to remove files.");
148
+ }
149
+ const result = await runUninstall(destinationDir, target, dryRun);
150
+ info(`Files removed/planned: ${result.removed.length}`);
151
+ info(`Files missing: ${result.missing.length}`);
152
+ if (dryRun) {
153
+ success("Uninstall dry-run completed.");
154
+ }
155
+ else {
156
+ success("Uninstall completed.");
157
+ }
158
+ }
159
+ catch (err) {
160
+ error(err.message);
161
+ process.exitCode = 1;
162
+ }
163
+ });
164
+ /**
165
+ * Runs interactive installer workflow when no subcommand is provided.
166
+ */
167
+ async function runInteractiveWizard() {
168
+ const projectDir = process.cwd();
169
+ const catalog = await loadSourceCatalog(projectDir);
170
+ const adapters = getPlatformAdapters();
171
+ const targetAnswer = await prompts({
172
+ type: "select",
173
+ name: "target",
174
+ message: "Выбери AI для установки:",
175
+ choices: adapters.map((adapter) => ({
176
+ title: `${adapter.label} (${adapter.id})`,
177
+ value: adapter.id,
178
+ description: adapter.description,
179
+ })),
180
+ });
181
+ if (!targetAnswer.target) {
182
+ warn("Установка отменена.");
183
+ return;
184
+ }
185
+ const destinationAnswer = await prompts({
186
+ type: "text",
187
+ name: "destination",
188
+ message: "Папка установки:",
189
+ initial: projectDir,
190
+ validate: (value) => (value.trim().length === 0 ? "Укажи путь" : true),
191
+ });
192
+ if (!destinationAnswer.destination) {
193
+ warn("Установка отменена.");
194
+ return;
195
+ }
196
+ const agents = listAgentNames(catalog);
197
+ const skills = listSkillNames(catalog);
198
+ const agentsAnswer = await prompts({
199
+ type: "multiselect",
200
+ name: "selectedAgents",
201
+ message: "Выбери агентов:",
202
+ choices: agents.map((agentName) => ({ title: agentName, value: agentName, selected: true })),
203
+ min: 1,
204
+ hint: "space: выбрать, enter: подтвердить",
205
+ });
206
+ if (!agentsAnswer.selectedAgents || agentsAnswer.selectedAgents.length === 0) {
207
+ warn("Установка отменена.");
208
+ return;
209
+ }
210
+ const skillsAnswer = await prompts({
211
+ type: "multiselect",
212
+ name: "selectedSkills",
213
+ message: "Выбери skills:",
214
+ choices: skills.map((skillName) => ({ title: skillName, value: skillName, selected: true })),
215
+ min: 1,
216
+ hint: "space: выбрать, enter: подтвердить",
217
+ });
218
+ if (!skillsAnswer.selectedSkills || skillsAnswer.selectedSkills.length === 0) {
219
+ warn("Установка отменена.");
220
+ return;
221
+ }
222
+ const optionsAnswer = await prompts([
223
+ {
224
+ type: "toggle",
225
+ name: "overwrite",
226
+ message: "Перезаписывать существующие файлы?",
227
+ initial: false,
228
+ active: "yes",
229
+ inactive: "no",
230
+ },
231
+ {
232
+ type: "toggle",
233
+ name: "apply",
234
+ message: "Сразу выполнить установку?",
235
+ initial: true,
236
+ active: "yes",
237
+ inactive: "no",
238
+ },
239
+ {
240
+ type: "toggle",
241
+ name: "strictHints",
242
+ message: "Требовать явные target-hints в agent/skill файлах?",
243
+ initial: false,
244
+ active: "yes",
245
+ inactive: "no",
246
+ },
247
+ ]);
248
+ const target = targetAnswer.target;
249
+ const destinationDir = path.resolve(destinationAnswer.destination);
250
+ info("Запускаю doctor перед установкой...");
251
+ const doctor = await runDoctor(projectDir, destinationDir, target);
252
+ for (const line of doctor.info) {
253
+ info(line);
254
+ }
255
+ for (const line of doctor.warnings) {
256
+ warn(line);
257
+ }
258
+ for (const line of doctor.errors) {
259
+ error(line);
260
+ }
261
+ if (doctor.errors.length > 0) {
262
+ throw new Error("Doctor не пройден. Исправь ошибки и запусти снова.");
263
+ }
264
+ const dryRun = !Boolean(optionsAnswer.apply);
265
+ const overwriteMode = optionsAnswer.overwrite ? "overwrite" : "skip";
266
+ if (dryRun) {
267
+ warn("Выбран preview-режим без записи файлов.");
268
+ }
269
+ const { state, result } = await runInstall({
270
+ target,
271
+ projectDir,
272
+ destinationDir,
273
+ selectedAgents: agentsAnswer.selectedAgents,
274
+ selectedSkills: skillsAnswer.selectedSkills,
275
+ dryRun,
276
+ overwriteMode,
277
+ strictHints: Boolean(optionsAnswer.strictHints),
278
+ });
279
+ info(`Target: ${state.target}`);
280
+ info(`Destination: ${state.destinationDir}`);
281
+ info(`Files planned: ${result.plannedFiles.length}`);
282
+ if (dryRun) {
283
+ info(`Files would write: ${result.writtenFiles.length}`);
284
+ success("Preview completed.");
285
+ }
286
+ else {
287
+ info(`Files written: ${result.writtenFiles.length}`);
288
+ if (result.backupDir) {
289
+ info(`Backup: ${result.backupDir}`);
290
+ }
291
+ success("Install completed.");
292
+ }
293
+ }
294
+ /**
295
+ * Normalizes user target aliases into strict target identifiers.
296
+ * @param rawTarget Raw target value from CLI.
297
+ * @returns Normalized target id.
298
+ */
299
+ function normalizeTarget(rawTarget) {
300
+ const value = rawTarget.trim().toLowerCase();
301
+ const aliasMap = {
302
+ "vscode-copilot": "vscode-copilot",
303
+ copilot: "vscode-copilot",
304
+ "gpt-codex": "gpt-codex",
305
+ codex: "gpt-codex",
306
+ gptcodex: "gpt-codex",
307
+ claude: "claude",
308
+ "qwen-3.5": "qwen-3.5",
309
+ qwen: "qwen-3.5",
310
+ "google-antugravity": "google-antugravity",
311
+ "google-antigravity": "google-antugravity",
312
+ antugravity: "google-antugravity",
313
+ antigravity: "google-antugravity",
314
+ google: "google-antugravity",
315
+ };
316
+ const normalized = aliasMap[value];
317
+ if (!normalized) {
318
+ throw new Error(`Unsupported target '${rawTarget}'. Run 'code-ai targets' for full list.`);
319
+ }
320
+ return normalized;
321
+ }
322
+ if (process.argv.length <= 2) {
323
+ runInteractiveWizard().catch((err) => {
324
+ error(err.message);
325
+ process.exit(1);
326
+ });
327
+ }
328
+ else {
329
+ program.parseAsync(process.argv).catch((err) => {
330
+ error(err.message);
331
+ process.exit(1);
332
+ });
333
+ }
@@ -0,0 +1,35 @@
1
+ import type { InstallOptions, InstallState } from "./types.js";
2
+ interface ApplyResult {
3
+ plannedFiles: string[];
4
+ writtenFiles: string[];
5
+ skippedFiles: string[];
6
+ backupDir?: string;
7
+ }
8
+ /**
9
+ * Runs installation for selected target with optional dry-run and backup.
10
+ * @param options Install options.
11
+ * @returns Install state and write summary.
12
+ */
13
+ export declare function runInstall(options: InstallOptions): Promise<{
14
+ state: InstallState;
15
+ result: ApplyResult;
16
+ }>;
17
+ /**
18
+ * Executes uninstall by reading and deleting previously installed files.
19
+ * @param destinationDir Destination root where state is stored.
20
+ * @param target Target id.
21
+ * @param dryRun Dry-run flag.
22
+ * @returns Deleted files list.
23
+ */
24
+ export declare function runUninstall(destinationDir: string, target: InstallState["target"], dryRun: boolean): Promise<{
25
+ removed: string[];
26
+ missing: string[];
27
+ }>;
28
+ /**
29
+ * Reads installation state file for target.
30
+ * @param destinationDir Destination root.
31
+ * @param target Target id.
32
+ * @returns Install state or null.
33
+ */
34
+ export declare function readInstallState(destinationDir: string, target: InstallState["target"]): Promise<InstallState | null>;
35
+ export {};
@@ -0,0 +1,205 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { getPlatformAdapter } from "./platforms/adapters.js";
4
+ import { loadSourceCatalog } from "./catalog.js";
5
+ import { hasExplicitTargetHint, transformContentForTarget } from "./contentTransformer.js";
6
+ /**
7
+ * Runs installation for selected target with optional dry-run and backup.
8
+ * @param options Install options.
9
+ * @returns Install state and write summary.
10
+ */
11
+ export async function runInstall(options) {
12
+ const catalog = await loadSourceCatalog(options.projectDir);
13
+ const adapter = getPlatformAdapter(options.target);
14
+ const operations = adapter.planOperations({
15
+ catalog,
16
+ destinationDir: options.destinationDir,
17
+ selectedAgents: options.selectedAgents,
18
+ selectedSkills: options.selectedSkills,
19
+ });
20
+ const state = {
21
+ target: options.target,
22
+ installedAt: new Date().toISOString(),
23
+ destinationDir: options.destinationDir,
24
+ projectDir: options.projectDir,
25
+ files: operations.map((op) => op.destinationPath),
26
+ selectedAgents: options.selectedAgents,
27
+ selectedSkills: options.selectedSkills,
28
+ };
29
+ const result = await applyOperations({
30
+ operations,
31
+ overwriteMode: options.overwriteMode,
32
+ dryRun: options.dryRun,
33
+ destinationDir: options.destinationDir,
34
+ target: options.target,
35
+ strictHints: options.strictHints,
36
+ });
37
+ if (!options.dryRun) {
38
+ await writeInstallState(state);
39
+ }
40
+ return { state, result };
41
+ }
42
+ /**
43
+ * Executes uninstall by reading and deleting previously installed files.
44
+ * @param destinationDir Destination root where state is stored.
45
+ * @param target Target id.
46
+ * @param dryRun Dry-run flag.
47
+ * @returns Deleted files list.
48
+ */
49
+ export async function runUninstall(destinationDir, target, dryRun) {
50
+ const state = await readInstallState(destinationDir, target);
51
+ if (!state) {
52
+ throw new Error(`No install state found for target ${target}.`);
53
+ }
54
+ const removed = [];
55
+ const missing = [];
56
+ for (const filePath of state.files) {
57
+ if (!(await fs.pathExists(filePath))) {
58
+ missing.push(filePath);
59
+ continue;
60
+ }
61
+ if (!dryRun) {
62
+ await fs.remove(filePath);
63
+ }
64
+ removed.push(filePath);
65
+ }
66
+ if (!dryRun) {
67
+ await deleteInstallState(destinationDir, target);
68
+ }
69
+ return { removed, missing };
70
+ }
71
+ /**
72
+ * Reads installation state file for target.
73
+ * @param destinationDir Destination root.
74
+ * @param target Target id.
75
+ * @returns Install state or null.
76
+ */
77
+ export async function readInstallState(destinationDir, target) {
78
+ const stateFile = getStateFilePath(destinationDir, target);
79
+ if (!(await fs.pathExists(stateFile))) {
80
+ return null;
81
+ }
82
+ return fs.readJson(stateFile);
83
+ }
84
+ /**
85
+ * Performs planned operations with backup and rollback semantics.
86
+ * @param args Execution settings.
87
+ * @returns Apply result summary.
88
+ */
89
+ async function applyOperations(args) {
90
+ const plannedFiles = [];
91
+ const writtenFiles = [];
92
+ const skippedFiles = [];
93
+ const backupMap = new Map();
94
+ let backupDir;
95
+ if (!args.dryRun) {
96
+ backupDir = await createBackupDir(args.destinationDir, args.target);
97
+ }
98
+ try {
99
+ for (const operation of args.operations) {
100
+ const destination = operation.destinationPath;
101
+ plannedFiles.push(destination);
102
+ const exists = await fs.pathExists(destination);
103
+ if (exists && args.overwriteMode === "skip") {
104
+ skippedFiles.push(destination);
105
+ continue;
106
+ }
107
+ let sourceContentForTransform;
108
+ if (operation.transform &&
109
+ args.strictHints &&
110
+ (operation.transform.assetType === "agent" || operation.transform.assetType === "skill")) {
111
+ sourceContentForTransform = await fs.readFile(operation.sourcePath, "utf8");
112
+ if (!hasExplicitTargetHint(sourceContentForTransform, operation.transform.target)) {
113
+ throw new Error(`Strict hints check failed: missing explicit '${operation.transform.target}' hint in ${operation.sourcePath}`);
114
+ }
115
+ }
116
+ if (!args.dryRun && exists && backupDir) {
117
+ const rel = path.relative(args.destinationDir, destination);
118
+ const backupPath = path.join(backupDir, rel);
119
+ await fs.ensureDir(path.dirname(backupPath));
120
+ await fs.copy(destination, backupPath, { overwrite: true });
121
+ backupMap.set(destination, backupPath);
122
+ }
123
+ if (!args.dryRun) {
124
+ await fs.ensureDir(path.dirname(destination));
125
+ if (operation.generated) {
126
+ await fs.writeFile(destination, operation.content ?? "", "utf8");
127
+ }
128
+ else if (operation.transform) {
129
+ const sourceContent = sourceContentForTransform ?? (await fs.readFile(operation.sourcePath, "utf8"));
130
+ const transformed = transformContentForTarget(sourceContent, operation.transform.target, operation.transform.assetType);
131
+ await fs.writeFile(destination, transformed, "utf8");
132
+ }
133
+ else {
134
+ await fs.copy(operation.sourcePath, destination, { overwrite: true });
135
+ }
136
+ }
137
+ writtenFiles.push(destination);
138
+ }
139
+ }
140
+ catch (error) {
141
+ if (!args.dryRun) {
142
+ await rollbackWrites(writtenFiles, backupMap);
143
+ }
144
+ throw error;
145
+ }
146
+ return { plannedFiles, writtenFiles, skippedFiles, backupDir };
147
+ }
148
+ /**
149
+ * Rolls back written files from backup map when apply fails.
150
+ * @param writtenFiles Files that were written in current transaction.
151
+ * @param backupMap Existing-file backup mapping.
152
+ */
153
+ async function rollbackWrites(writtenFiles, backupMap) {
154
+ for (const destination of writtenFiles.slice().reverse()) {
155
+ const backupPath = backupMap.get(destination);
156
+ if (backupPath && (await fs.pathExists(backupPath))) {
157
+ await fs.copy(backupPath, destination, { overwrite: true });
158
+ continue;
159
+ }
160
+ if (await fs.pathExists(destination)) {
161
+ await fs.remove(destination);
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Writes install state for future uninstall operations.
167
+ * @param state Install state.
168
+ */
169
+ async function writeInstallState(state) {
170
+ const stateFile = getStateFilePath(state.destinationDir, state.target);
171
+ await fs.ensureDir(path.dirname(stateFile));
172
+ await fs.writeJson(stateFile, state, { spaces: 2 });
173
+ }
174
+ /**
175
+ * Deletes stored state file for the given target.
176
+ * @param destinationDir Destination root.
177
+ * @param target Target id.
178
+ */
179
+ async function deleteInstallState(destinationDir, target) {
180
+ const stateFile = getStateFilePath(destinationDir, target);
181
+ if (await fs.pathExists(stateFile)) {
182
+ await fs.remove(stateFile);
183
+ }
184
+ }
185
+ /**
186
+ * Returns absolute path to state file for the target.
187
+ * @param destinationDir Destination root.
188
+ * @param target Target id.
189
+ * @returns State file path.
190
+ */
191
+ function getStateFilePath(destinationDir, target) {
192
+ return path.join(destinationDir, ".code-ai-installer", "state", `${target}.json`);
193
+ }
194
+ /**
195
+ * Creates a timestamped backup directory for one install run.
196
+ * @param destinationDir Destination root.
197
+ * @param target Target id.
198
+ * @returns Backup directory path.
199
+ */
200
+ async function createBackupDir(destinationDir, target) {
201
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
202
+ const backupDir = path.join(destinationDir, ".code-ai-installer", "backups", target, timestamp);
203
+ await fs.ensureDir(backupDir);
204
+ return backupDir;
205
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Prints informational line.
3
+ * @param message Message text.
4
+ */
5
+ export declare function info(message: string): void;
6
+ /**
7
+ * Prints warning line.
8
+ * @param message Message text.
9
+ */
10
+ export declare function warn(message: string): void;
11
+ /**
12
+ * Prints error line.
13
+ * @param message Message text.
14
+ */
15
+ export declare function error(message: string): void;
16
+ /**
17
+ * Prints success line.
18
+ * @param message Message text.
19
+ */
20
+ export declare function success(message: string): void;
package/dist/logger.js ADDED
@@ -0,0 +1,29 @@
1
+ import chalk from "chalk";
2
+ /**
3
+ * Prints informational line.
4
+ * @param message Message text.
5
+ */
6
+ export function info(message) {
7
+ process.stdout.write(`${chalk.cyan("info")} ${message}\n`);
8
+ }
9
+ /**
10
+ * Prints warning line.
11
+ * @param message Message text.
12
+ */
13
+ export function warn(message) {
14
+ process.stdout.write(`${chalk.yellow("warn")} ${message}\n`);
15
+ }
16
+ /**
17
+ * Prints error line.
18
+ * @param message Message text.
19
+ */
20
+ export function error(message) {
21
+ process.stderr.write(`${chalk.red("error")} ${message}\n`);
22
+ }
23
+ /**
24
+ * Prints success line.
25
+ * @param message Message text.
26
+ */
27
+ export function success(message) {
28
+ process.stdout.write(`${chalk.green("ok")} ${message}\n`);
29
+ }
@@ -0,0 +1,12 @@
1
+ import type { PlatformAdapter, TargetId } from "../types.js";
2
+ /**
3
+ * Returns all supported platform adapters.
4
+ * @returns Array of platform adapters.
5
+ */
6
+ export declare function getPlatformAdapters(): PlatformAdapter[];
7
+ /**
8
+ * Returns a single platform adapter by identifier.
9
+ * @param target Target id.
10
+ * @returns Platform adapter.
11
+ */
12
+ export declare function getPlatformAdapter(target: TargetId): PlatformAdapter;