@automagik/genie 4.260331.6 → 4.260331.8

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.
@@ -1,342 +0,0 @@
1
- /**
2
- * Manifest — Genie item manifest parsing, validation, and detection.
3
- *
4
- * Every registry item (agent, skill, app, board, workflow, stack, template, hook)
5
- * is described by a `genie.yaml` manifest. This module handles:
6
- * - Parsing YAML manifests into typed GenieManifest objects
7
- * - Validating manifests against type-specific rules
8
- * - Auto-detecting manifests from directory conventions (AGENTS.md, skill.md, etc.)
9
- */
10
-
11
- import { existsSync, readFileSync } from 'node:fs';
12
- import { basename, join } from 'node:path';
13
- import * as yaml from 'js-yaml';
14
-
15
- // ============================================================================
16
- // Types
17
- // ============================================================================
18
-
19
- export type ItemType = 'agent' | 'skill' | 'app' | 'board' | 'workflow' | 'stack' | 'template' | 'hook';
20
-
21
- const ITEM_TYPES: ItemType[] = ['agent', 'skill', 'app', 'board', 'workflow', 'stack', 'template', 'hook'];
22
-
23
- export interface StageConfig {
24
- name: string;
25
- label?: string;
26
- gate: 'human' | 'agent' | 'human+agent';
27
- action?: string;
28
- auto_advance?: boolean;
29
- roles?: string[];
30
- color?: string;
31
- }
32
-
33
- export interface StackItem {
34
- name: string;
35
- type: ItemType;
36
- source?: string;
37
- inline?: boolean;
38
- config?: Record<string, unknown>;
39
- }
40
-
41
- export interface GenieManifest {
42
- name: string;
43
- version: string;
44
- type: ItemType;
45
- description?: string;
46
- author?: { name: string; url?: string };
47
- agent?: { model?: string; promptMode?: string; roles?: string[]; entrypoint: string };
48
- skill?: { triggers?: string[]; entrypoint: string };
49
- app?: { runtime?: string; natsPrefix?: string; icon?: string; entrypoint: string };
50
- board?: { stages: StageConfig[] };
51
- workflow?: { cron: string; timezone?: string; command: string; run_spec?: Record<string, unknown> };
52
- stack?: { items: StackItem[] };
53
- dependencies?: string[];
54
- tags?: string[];
55
- category?: string;
56
- license?: string;
57
- }
58
-
59
- interface ValidationResult {
60
- valid: boolean;
61
- errors: string[];
62
- warnings: string[];
63
- }
64
-
65
- // ============================================================================
66
- // Parsing
67
- // ============================================================================
68
-
69
- /**
70
- * Parse a YAML string into a typed GenieManifest.
71
- *
72
- * Validates that required fields (name, type, version) are present and that
73
- * the type value is a recognized ItemType. Throws on invalid input.
74
- */
75
- function parseManifest(yamlContent: string): GenieManifest {
76
- const raw = yaml.load(yamlContent);
77
- if (!raw || typeof raw !== 'object') {
78
- throw new Error('Manifest YAML is empty or not an object');
79
- }
80
-
81
- const obj = raw as Record<string, unknown>;
82
-
83
- if (!obj.name || typeof obj.name !== 'string') {
84
- throw new Error('Manifest is missing required field: name');
85
- }
86
- if (!obj.type || typeof obj.type !== 'string') {
87
- throw new Error('Manifest is missing required field: type');
88
- }
89
- if (!obj.version || typeof obj.version !== 'string') {
90
- throw new Error('Manifest is missing required field: version');
91
- }
92
-
93
- if (!ITEM_TYPES.includes(obj.type as ItemType)) {
94
- throw new Error(`Invalid item type "${obj.type}". Must be one of: ${ITEM_TYPES.join(', ')}`);
95
- }
96
-
97
- return obj as unknown as GenieManifest;
98
- }
99
-
100
- // ============================================================================
101
- // Validation
102
- // ============================================================================
103
-
104
- const VALID_GATES = new Set(['human', 'agent', 'human+agent']);
105
-
106
- /**
107
- * Validate a manifest against type-specific rules.
108
- *
109
- * Checks required fields, type-specific sections, entrypoint file existence,
110
- * board stage structure, cron format, and stack item completeness.
111
- * Returns a ValidationResult with errors (blockers) and warnings (advisory).
112
- */
113
- export function validateManifest(manifest: GenieManifest, itemDir: string): ValidationResult {
114
- const errors: string[] = [];
115
- const warnings: string[] = [];
116
-
117
- // Required top-level fields
118
- if (!manifest.name) errors.push('Missing required field: name');
119
- if (!manifest.type) errors.push('Missing required field: type');
120
- if (!manifest.version) errors.push('Missing required field: version');
121
-
122
- if (manifest.type && !ITEM_TYPES.includes(manifest.type)) {
123
- errors.push(`Invalid item type "${manifest.type}". Must be one of: ${ITEM_TYPES.join(', ')}`);
124
- }
125
-
126
- // Type-specific validation
127
- const { type } = manifest;
128
-
129
- // Warn if the type-specific section is missing
130
- if (type && !manifest[type as keyof GenieManifest]) {
131
- warnings.push(`Manifest type is "${type}" but no "${type}" section is defined`);
132
- }
133
-
134
- // Agent validation
135
- if (manifest.agent) {
136
- validateEntrypoint(manifest.agent.entrypoint, itemDir, 'agent', errors);
137
- }
138
-
139
- // Skill validation
140
- if (manifest.skill) {
141
- validateEntrypoint(manifest.skill.entrypoint, itemDir, 'skill', errors);
142
- }
143
-
144
- // App validation
145
- if (manifest.app) {
146
- validateEntrypoint(manifest.app.entrypoint, itemDir, 'app', errors);
147
- }
148
-
149
- // Board validation
150
- if (manifest.board) {
151
- validateStages(manifest.board.stages, errors);
152
- }
153
-
154
- // Workflow validation
155
- if (manifest.workflow) {
156
- validateCron(manifest.workflow.cron, errors);
157
- }
158
-
159
- // Stack validation
160
- if (manifest.stack) {
161
- validateStackItems(manifest.stack.items, errors);
162
- }
163
-
164
- return { valid: errors.length === 0, errors, warnings };
165
- }
166
-
167
- /** Check that an entrypoint file exists on disk. */
168
- function validateEntrypoint(entrypoint: string | undefined, itemDir: string, section: string, errors: string[]): void {
169
- if (!entrypoint) {
170
- errors.push(`${section} section is missing required field: entrypoint`);
171
- return;
172
- }
173
- const fullPath = join(itemDir, entrypoint);
174
- if (!existsSync(fullPath)) {
175
- errors.push(`${section} entrypoint not found: ${entrypoint} (expected at ${fullPath})`);
176
- }
177
- }
178
-
179
- /** Validate board stage definitions. */
180
- function validateStages(stages: StageConfig[] | undefined, errors: string[]): void {
181
- if (!stages || !Array.isArray(stages)) {
182
- errors.push('board section is missing required field: stages');
183
- return;
184
- }
185
- for (let i = 0; i < stages.length; i++) {
186
- const stage = stages[i];
187
- if (!stage.name) {
188
- errors.push(`board.stages[${i}] is missing required field: name`);
189
- }
190
- if (!stage.gate) {
191
- errors.push(`board.stages[${i}] is missing required field: gate`);
192
- } else if (!VALID_GATES.has(stage.gate)) {
193
- errors.push(`board.stages[${i}].gate "${stage.gate}" is invalid. Must be one of: human, agent, human+agent`);
194
- }
195
- }
196
- }
197
-
198
- /**
199
- * Validate a cron expression has 5 or 6 space-separated fields.
200
- * This is a basic format check, not full cron syntax validation.
201
- */
202
- function validateCron(cron: string | undefined, errors: string[]): void {
203
- if (!cron) {
204
- errors.push('workflow section is missing required field: cron');
205
- return;
206
- }
207
- const fields = cron.trim().split(/\s+/);
208
- if (fields.length < 5 || fields.length > 6) {
209
- errors.push(`workflow.cron "${cron}" is invalid: expected 5 or 6 space-separated fields, got ${fields.length}`);
210
- }
211
- }
212
-
213
- /** Validate stack item definitions. */
214
- function validateStackItems(items: StackItem[] | undefined, errors: string[]): void {
215
- if (!items || !Array.isArray(items)) {
216
- errors.push('stack section is missing required field: items');
217
- return;
218
- }
219
- for (let i = 0; i < items.length; i++) {
220
- const item = items[i];
221
- if (!item.type) {
222
- errors.push(`stack.items[${i}] is missing required field: type`);
223
- }
224
- if (!item.source && !item.inline) {
225
- errors.push(`stack.items[${i}] must have either "source" or "inline: true"`);
226
- }
227
- }
228
- }
229
-
230
- // ============================================================================
231
- // Detection
232
- // ============================================================================
233
-
234
- /**
235
- * Detect a manifest from a directory using a fallback chain.
236
- *
237
- * Detection order:
238
- * 1. `genie.yaml` — explicit manifest file
239
- * 2. `AGENTS.md` — infer agent manifest from YAML frontmatter
240
- * 3. `manifest.ts` — infer app manifest
241
- * 4. `skill.md` — infer skill manifest
242
- * 5. None found — return error
243
- */
244
- export async function detectManifest(
245
- dir: string,
246
- ): Promise<{ manifest: GenieManifest; source: string } | { error: string }> {
247
- // 1. Explicit genie.yaml
248
- const yamlPath = join(dir, 'genie.yaml');
249
- if (existsSync(yamlPath)) {
250
- try {
251
- const content = readFileSync(yamlPath, 'utf-8');
252
- const manifest = parseManifest(content);
253
- return { manifest, source: 'genie.yaml' };
254
- } catch (err) {
255
- return { error: `Failed to parse genie.yaml: ${(err as Error).message}` };
256
- }
257
- }
258
-
259
- const dirName = basename(dir);
260
-
261
- // 2. AGENTS.md — infer agent manifest from frontmatter
262
- const agentsPath = join(dir, 'AGENTS.md');
263
- if (existsSync(agentsPath)) {
264
- const manifest = inferAgentManifest(agentsPath, dirName);
265
- return { manifest, source: 'AGENTS.md' };
266
- }
267
-
268
- // 3. manifest.ts — infer app manifest
269
- const manifestTsPath = join(dir, 'manifest.ts');
270
- if (existsSync(manifestTsPath)) {
271
- const manifest: GenieManifest = {
272
- name: dirName,
273
- type: 'app',
274
- version: '0.0.0',
275
- };
276
- return { manifest, source: 'manifest.ts' };
277
- }
278
-
279
- // 4. skill.md — infer skill manifest
280
- const skillPath = join(dir, 'skill.md');
281
- if (existsSync(skillPath)) {
282
- const manifest: GenieManifest = {
283
- name: dirName,
284
- type: 'skill',
285
- version: '0.0.0',
286
- };
287
- return { manifest, source: 'skill.md' };
288
- }
289
-
290
- // 5. Nothing found
291
- return {
292
- error:
293
- 'No manifest found. Create a genie.yaml or use a recognized file pattern (AGENTS.md, manifest.ts, skill.md).',
294
- };
295
- }
296
-
297
- /**
298
- * Extract YAML frontmatter from a markdown file.
299
- *
300
- * Looks for content between `---` markers at the top of the file.
301
- * Returns the parsed object or null if no frontmatter is found.
302
- */
303
- function extractFrontmatter(filePath: string): Record<string, unknown> | null {
304
- const content = readFileSync(filePath, 'utf-8');
305
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
306
- if (!match) return null;
307
-
308
- try {
309
- const parsed = yaml.load(match[1]);
310
- if (parsed && typeof parsed === 'object') {
311
- return parsed as Record<string, unknown>;
312
- }
313
- } catch {
314
- // Frontmatter is not valid YAML — ignore
315
- }
316
- return null;
317
- }
318
-
319
- /** Build an agent manifest from AGENTS.md frontmatter. */
320
- function inferAgentManifest(agentsPath: string, dirName: string): GenieManifest {
321
- const frontmatter = extractFrontmatter(agentsPath);
322
-
323
- const name = (frontmatter?.name as string) || dirName;
324
- const model = frontmatter?.model as string | undefined;
325
- const roles = Array.isArray(frontmatter?.roles) ? (frontmatter.roles as string[]) : undefined;
326
-
327
- const manifest: GenieManifest = {
328
- name,
329
- type: 'agent',
330
- version: '0.0.0',
331
- };
332
-
333
- if (model || roles) {
334
- manifest.agent = {
335
- entrypoint: 'AGENTS.md',
336
- ...(model && { model }),
337
- ...(roles && { roles }),
338
- };
339
- }
340
-
341
- return manifest;
342
- }
@@ -1,372 +0,0 @@
1
- /**
2
- * Install Command — `genie install <git-url>[@version]`
3
- *
4
- * Clones a git repository, detects/validates the manifest, registers in
5
- * app_store, and performs type-specific setup (cache regen, board creation, etc.).
6
- */
7
-
8
- import { execSync } from 'node:child_process';
9
- import { existsSync, mkdirSync, rmSync } from 'node:fs';
10
- import { homedir } from 'node:os';
11
- import { basename, join } from 'node:path';
12
- import type { Command } from 'commander';
13
- import {
14
- getItemFromStore,
15
- regenerateAgentCache,
16
- registerItemInStore,
17
- removeItemFromStore,
18
- } from '../lib/agent-cache.js';
19
- import { getActor, recordAuditEvent } from '../lib/audit.js';
20
- import { getConnection, isAvailable } from '../lib/db.js';
21
- import { type GenieManifest, type StageConfig, detectManifest, validateManifest } from '../lib/manifest.js';
22
-
23
- const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
24
- const ITEMS_DIR = join(GENIE_HOME, 'items');
25
-
26
- // ============================================================================
27
- // URL parsing
28
- // ============================================================================
29
-
30
- interface ParsedUrl {
31
- url: string;
32
- version?: string;
33
- name: string;
34
- }
35
-
36
- /**
37
- * Parse a git install target like `github.com/user/repo@v1.2.0`
38
- * into its components.
39
- */
40
- function parseInstallTarget(target: string): ParsedUrl {
41
- let url = target;
42
- let version: string | undefined;
43
-
44
- // Extract @version suffix
45
- const atIdx = url.lastIndexOf('@');
46
- if (atIdx > 0 && !url.slice(atIdx).includes('/')) {
47
- version = url.slice(atIdx + 1);
48
- url = url.slice(0, atIdx);
49
- }
50
-
51
- // Normalise bare github.com/user/repo → https://github.com/user/repo.git
52
- if (!url.startsWith('http') && !url.startsWith('git@') && !url.startsWith('ssh://')) {
53
- url = `https://${url}`;
54
- }
55
- if (url.startsWith('https://') && !url.endsWith('.git')) {
56
- url = `${url}.git`;
57
- }
58
-
59
- // Derive name from repo URL
60
- const name = basename(url, '.git');
61
-
62
- return { url, version, name };
63
- }
64
-
65
- // ============================================================================
66
- // Git operations
67
- // ============================================================================
68
-
69
- function cloneRepo(url: string, dest: string, options: { shallow?: boolean; version?: string }): void {
70
- const args = ['git', 'clone'];
71
- if (options.shallow !== false) args.push('--depth', '1');
72
- if (options.version) args.push('--branch', options.version);
73
- args.push(url, dest);
74
-
75
- execSync(args.join(' '), { stdio: 'pipe', timeout: 120_000 });
76
- }
77
-
78
- function cleanupDir(dir: string): void {
79
- if (existsSync(dir)) {
80
- rmSync(dir, { recursive: true, force: true });
81
- }
82
- }
83
-
84
- // ============================================================================
85
- // Type-specific registration
86
- // ============================================================================
87
-
88
- async function registerByType(manifest: GenieManifest, installPath: string): Promise<void> {
89
- switch (manifest.type) {
90
- case 'agent':
91
- await regenerateAgentCache();
92
- break;
93
- case 'board':
94
- await registerBoard(manifest);
95
- break;
96
- case 'workflow':
97
- await registerWorkflow(manifest);
98
- break;
99
- case 'stack':
100
- await installStack(manifest, installPath);
101
- break;
102
- // skill, app, template, hook — no extra registration needed beyond app_store
103
- }
104
- }
105
-
106
- async function registerBoard(manifest: GenieManifest): Promise<void> {
107
- if (!manifest.board?.stages) return;
108
- if (!(await isAvailable())) return;
109
-
110
- const sql = await getConnection();
111
- const stages = manifest.board.stages.map((s, i) => ({
112
- id: crypto.randomUUID(),
113
- name: s.name,
114
- label: s.label ?? s.name,
115
- gate: s.gate,
116
- action: s.action ?? null,
117
- auto_advance: s.auto_advance ?? false,
118
- roles: s.roles ?? ['*'],
119
- color: s.color ?? '#94a3b8',
120
- parallel: false,
121
- on_fail: null,
122
- position: i,
123
- transitions: [],
124
- }));
125
-
126
- await sql`
127
- INSERT INTO task_types (id, name, description, stages, is_builtin)
128
- VALUES (
129
- ${manifest.name},
130
- ${manifest.name},
131
- ${manifest.description ?? null},
132
- ${sql.json(stages)},
133
- false
134
- )
135
- ON CONFLICT (id) DO UPDATE SET
136
- stages = EXCLUDED.stages,
137
- description = EXCLUDED.description,
138
- updated_at = now()
139
- `;
140
- }
141
-
142
- async function registerWorkflow(manifest: GenieManifest): Promise<void> {
143
- if (!manifest.workflow) return;
144
- if (!(await isAvailable())) return;
145
-
146
- const sql = await getConnection();
147
- const wf = manifest.workflow;
148
-
149
- await sql`
150
- INSERT INTO schedules (id, name, cron_expression, timezone, command, run_spec, status)
151
- VALUES (
152
- ${`sched-${manifest.name}`},
153
- ${manifest.name},
154
- ${wf.cron},
155
- ${wf.timezone ?? 'UTC'},
156
- ${wf.command},
157
- ${sql.json(wf.run_spec ?? {})},
158
- 'active'
159
- )
160
- ON CONFLICT (id) DO UPDATE SET
161
- cron_expression = EXCLUDED.cron_expression,
162
- timezone = EXCLUDED.timezone,
163
- command = EXCLUDED.command,
164
- run_spec = EXCLUDED.run_spec,
165
- updated_at = now()
166
- `;
167
- }
168
-
169
- async function handleInlineStackItem(
170
- item: import('../lib/manifest.js').StackItem,
171
- version: string,
172
- tx: Awaited<ReturnType<typeof getConnection>>,
173
- ): Promise<void> {
174
- if (item.type === 'board' && item.config) {
175
- await registerBoard({
176
- name: item.name,
177
- type: 'board',
178
- version,
179
- board: { stages: (item.config.stages ?? []) as StageConfig[] },
180
- });
181
- } else if (item.type === 'workflow' && item.config) {
182
- await registerWorkflow({
183
- name: item.name,
184
- type: 'workflow',
185
- version,
186
- workflow: item.config as unknown as NonNullable<GenieManifest['workflow']>,
187
- });
188
- }
189
- await tx`
190
- INSERT INTO app_store (name, item_type, version, manifest)
191
- VALUES (${item.name}, ${item.type}, ${version}, ${tx.json(item.config ?? {})})
192
- ON CONFLICT (name) DO NOTHING
193
- `;
194
- }
195
-
196
- async function handleExternalStackItem(
197
- item: import('../lib/manifest.js').StackItem,
198
- tx: Awaited<ReturnType<typeof getConnection>>,
199
- ): Promise<string> {
200
- if (!item.source) throw new Error(`Stack item "${item.name}" is missing source`);
201
- const parsed = parseInstallTarget(item.source);
202
- const itemDir = join(ITEMS_DIR, parsed.name);
203
- mkdirSync(ITEMS_DIR, { recursive: true });
204
- cloneRepo(parsed.url, itemDir, { version: parsed.version });
205
-
206
- const detection = await detectManifest(itemDir);
207
- if ('error' in detection) {
208
- throw new Error(`Stack item "${item.name}" (${item.source}): ${detection.error}`);
209
- }
210
- const validation = validateManifest(detection.manifest, itemDir);
211
- if (!validation.valid) {
212
- throw new Error(`Stack item "${item.name}" validation failed: ${validation.errors.join(', ')}`);
213
- }
214
-
215
- await tx`
216
- INSERT INTO app_store (name, item_type, version, git_url, install_path, manifest)
217
- VALUES (
218
- ${detection.manifest.name}, ${detection.manifest.type}, ${detection.manifest.version},
219
- ${item.source}, ${itemDir}, ${tx.json(detection.manifest)}
220
- )
221
- ON CONFLICT (name) DO NOTHING
222
- `;
223
- await registerByType(detection.manifest, itemDir);
224
- return parsed.name;
225
- }
226
-
227
- async function installStack(manifest: GenieManifest, _installPath: string): Promise<void> {
228
- const stackItems = manifest.stack?.items;
229
- if (!stackItems) return;
230
- if (!(await isAvailable())) return;
231
-
232
- const sql = await getConnection();
233
- const installed: string[] = [];
234
-
235
- try {
236
- await sql.begin(async (tx: typeof sql) => {
237
- for (const item of stackItems) {
238
- if (item.inline) {
239
- await handleInlineStackItem(item, manifest.version, tx);
240
- installed.push(item.name);
241
- } else if (item.source) {
242
- const name = await handleExternalStackItem(item, tx);
243
- installed.push(name);
244
- }
245
- }
246
- });
247
- } catch (err) {
248
- // Rollback: clean up cloned dirs
249
- for (const name of installed) {
250
- cleanupDir(join(ITEMS_DIR, name));
251
- await removeItemFromStore(name).catch(() => {});
252
- }
253
- throw err;
254
- }
255
- }
256
-
257
- // ============================================================================
258
- // Install command
259
- // ============================================================================
260
-
261
- interface InstallOptions {
262
- force?: boolean;
263
- full?: boolean;
264
- }
265
-
266
- async function handleInstall(target: string, options: InstallOptions): Promise<void> {
267
- const parsed = parseInstallTarget(target);
268
- const installDir = join(ITEMS_DIR, parsed.name);
269
-
270
- // Check for name conflict
271
- const existing = await getItemFromStore(parsed.name).catch(() => null);
272
- if (existing && !options.force) {
273
- console.error(`Item "${parsed.name}" is already installed. Use --force to override.`);
274
- process.exit(1);
275
- }
276
- if (existing && options.force) {
277
- await removeItemFromStore(parsed.name).catch(() => {});
278
- cleanupDir(installDir);
279
- }
280
-
281
- // Clone
282
- mkdirSync(ITEMS_DIR, { recursive: true });
283
- console.log(`Cloning ${parsed.url}${parsed.version ? ` @ ${parsed.version}` : ''}...`);
284
- try {
285
- cloneRepo(parsed.url, installDir, { shallow: !options.full, version: parsed.version });
286
- } catch (err) {
287
- cleanupDir(installDir);
288
- console.error(`Clone failed: ${(err as Error).message}`);
289
- process.exit(1);
290
- }
291
-
292
- // Detect manifest
293
- const detection = await detectManifest(installDir);
294
- if ('error' in detection) {
295
- cleanupDir(installDir);
296
- console.error(`Manifest detection failed: ${detection.error}`);
297
- process.exit(1);
298
- }
299
-
300
- const { manifest, source } = detection;
301
- console.log(`Detected ${manifest.type} manifest from ${source}`);
302
-
303
- // Validate
304
- const validation = validateManifest(manifest, installDir);
305
- for (const w of validation.warnings) {
306
- console.log(` Warning: ${w}`);
307
- }
308
- if (!validation.valid) {
309
- cleanupDir(installDir);
310
- console.error(`Validation failed:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}`);
311
- process.exit(1);
312
- }
313
-
314
- // Register in app_store
315
- try {
316
- await registerItemInStore({
317
- name: manifest.name,
318
- itemType: manifest.type,
319
- version: manifest.version,
320
- description: manifest.description,
321
- authorName: manifest.author?.name,
322
- authorUrl: manifest.author?.url,
323
- gitUrl: parsed.url,
324
- installPath: installDir,
325
- manifest: manifest as unknown as Record<string, unknown>,
326
- tags: manifest.tags,
327
- category: manifest.category,
328
- license: manifest.license,
329
- dependencies: manifest.dependencies,
330
- });
331
- } catch (err) {
332
- cleanupDir(installDir);
333
- console.error(`Registration failed: ${(err as Error).message}`);
334
- process.exit(1);
335
- }
336
-
337
- // Type-specific registration
338
- await registerByType(manifest, installDir);
339
-
340
- // Audit
341
- recordAuditEvent('item', manifest.name, 'item_installed', getActor(), {
342
- type: manifest.type,
343
- version: manifest.version,
344
- source: parsed.url,
345
- manifestSource: source,
346
- }).catch(() => {});
347
-
348
- console.log(`\nInstalled ${manifest.type} "${manifest.name}" v${manifest.version}`);
349
- console.log(` Source: ${parsed.url}`);
350
- console.log(` Path: ${installDir}`);
351
- }
352
-
353
- // ============================================================================
354
- // Command registration
355
- // ============================================================================
356
-
357
- export function registerInstallCommand(program: Command): void {
358
- program
359
- .command('install <target>')
360
- .description('Install a genie item from a git URL (e.g. github.com/user/repo[@version])')
361
- .option('--force', 'Override existing item with same name')
362
- .option('--full', 'Full git clone instead of shallow')
363
- .action(async (target: string, options: InstallOptions) => {
364
- try {
365
- await handleInstall(target, options);
366
- } catch (error) {
367
- const message = error instanceof Error ? error.message : String(error);
368
- console.error(`Error: ${message}`);
369
- process.exit(1);
370
- }
371
- });
372
- }