@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,118 +0,0 @@
1
- /**
2
- * Uninstall Command — `genie uninstall <name>`
3
- *
4
- * Removes a previously installed item: deregisters from app_store,
5
- * performs type-specific cleanup, and removes the cloned directory.
6
- */
7
-
8
- import { existsSync, readFileSync, rmSync } from 'node:fs';
9
- import { homedir } from 'node:os';
10
- import { join } from 'node:path';
11
- import type { Command } from 'commander';
12
- import { getItemFromStore, regenerateAgentCache, removeItemFromStore } from '../lib/agent-cache.js';
13
- import { getActor, recordAuditEvent } from '../lib/audit.js';
14
- import { getConnection, isAvailable } from '../lib/db.js';
15
-
16
- const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
17
- const ITEMS_DIR = join(GENIE_HOME, 'items');
18
-
19
- // ============================================================================
20
- // Type-specific deregistration
21
- // ============================================================================
22
-
23
- async function deregisterByType(itemType: string, name: string): Promise<void> {
24
- if (!(await isAvailable())) return;
25
- const sql = await getConnection();
26
-
27
- switch (itemType) {
28
- case 'agent':
29
- // Remove from legacy agents table too
30
- await sql`DELETE FROM agents WHERE id = ${`dir:${name}`}`.catch(() => {});
31
- await regenerateAgentCache();
32
- break;
33
- case 'board':
34
- await sql`DELETE FROM task_types WHERE id = ${name}`.catch(() => {});
35
- break;
36
- case 'workflow':
37
- await sql`DELETE FROM schedules WHERE id = ${`sched-${name}`}`.catch(() => {});
38
- break;
39
- case 'app':
40
- await sql`DELETE FROM installed_apps WHERE app_store_id IN (
41
- SELECT id FROM app_store WHERE name = ${name}
42
- )`.catch(() => {});
43
- break;
44
- case 'stack': {
45
- // Try to read manifest and uninstall sub-items
46
- const manifestPath = join(ITEMS_DIR, name, 'genie.yaml');
47
- if (existsSync(manifestPath)) {
48
- try {
49
- const yaml = await import('js-yaml');
50
- const raw = yaml.load(readFileSync(manifestPath, 'utf-8')) as Record<string, unknown>;
51
- const stack = raw.stack as { items?: Array<{ name: string; type: string }> } | undefined;
52
- if (stack?.items) {
53
- for (const item of stack.items) {
54
- await deregisterByType(item.type, item.name);
55
- await removeItemFromStore(item.name).catch(() => {});
56
- }
57
- }
58
- } catch {
59
- // Best effort
60
- }
61
- }
62
- break;
63
- }
64
- }
65
- }
66
-
67
- // ============================================================================
68
- // Uninstall handler
69
- // ============================================================================
70
-
71
- async function handleUninstall(name: string): Promise<void> {
72
- const existing = await getItemFromStore(name).catch(() => null);
73
- if (!existing) {
74
- console.error(`Item "${name}" is not installed.`);
75
- process.exit(1);
76
- }
77
-
78
- const itemType = existing.item_type;
79
-
80
- // Type-specific deregistration
81
- await deregisterByType(itemType, name);
82
-
83
- // Remove from app_store
84
- await removeItemFromStore(name);
85
-
86
- // Remove cloned directory
87
- const installDir = join(ITEMS_DIR, name);
88
- if (existsSync(installDir)) {
89
- rmSync(installDir, { recursive: true, force: true });
90
- }
91
-
92
- // Audit
93
- recordAuditEvent('item', name, 'item_uninstalled', getActor(), {
94
- type: itemType,
95
- version: existing.version,
96
- }).catch(() => {});
97
-
98
- console.log(`Uninstalled ${itemType} "${name}".`);
99
- }
100
-
101
- // ============================================================================
102
- // Command registration
103
- // ============================================================================
104
-
105
- export function registerItemUninstallCommand(parent: Command): void {
106
- parent
107
- .command('uninstall <name>')
108
- .description('Remove an installed genie item')
109
- .action(async (name: string) => {
110
- try {
111
- await handleUninstall(name);
112
- } catch (error) {
113
- const message = error instanceof Error ? error.message : String(error);
114
- console.error(`Error: ${message}`);
115
- process.exit(1);
116
- }
117
- });
118
- }
@@ -1,205 +0,0 @@
1
- /**
2
- * Update Command — `genie update <name>` or `genie update --all`
3
- *
4
- * Pulls the latest version (or a specific tag) of an installed item,
5
- * re-validates its manifest, and updates the app_store entry.
6
- */
7
-
8
- import { execSync } from 'node:child_process';
9
- import { existsSync } from 'node:fs';
10
- import { homedir } from 'node:os';
11
- import { join } from 'node:path';
12
- import type { Command } from 'commander';
13
- import { getItemFromStore, listItemsFromStore, regenerateAgentCache, updateItemInStore } from '../lib/agent-cache.js';
14
- import { getActor, recordAuditEvent } from '../lib/audit.js';
15
- import { getConnection, isAvailable } from '../lib/db.js';
16
- import { type GenieManifest, detectManifest, validateManifest } from '../lib/manifest.js';
17
-
18
- const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
19
- const ITEMS_DIR = join(GENIE_HOME, 'items');
20
-
21
- // ============================================================================
22
- // Type-specific re-registration
23
- // ============================================================================
24
-
25
- async function reregisterByType(manifest: GenieManifest): Promise<void> {
26
- if (!(await isAvailable())) return;
27
- const sql = await getConnection();
28
-
29
- switch (manifest.type) {
30
- case 'agent':
31
- await regenerateAgentCache();
32
- break;
33
- case 'board':
34
- if (manifest.board?.stages) {
35
- const stages = manifest.board.stages.map((s, i) => ({
36
- id: crypto.randomUUID(),
37
- name: s.name,
38
- label: s.label ?? s.name,
39
- gate: s.gate,
40
- action: s.action ?? null,
41
- auto_advance: s.auto_advance ?? false,
42
- roles: s.roles ?? ['*'],
43
- color: s.color ?? '#94a3b8',
44
- parallel: false,
45
- on_fail: null,
46
- position: i,
47
- transitions: [],
48
- }));
49
- await sql`
50
- UPDATE task_types SET stages = ${sql.json(stages)}, updated_at = now()
51
- WHERE id = ${manifest.name}
52
- `.catch(() => {});
53
- }
54
- break;
55
- case 'workflow':
56
- if (manifest.workflow) {
57
- await sql`
58
- UPDATE schedules SET
59
- cron_expression = ${manifest.workflow.cron},
60
- timezone = ${manifest.workflow.timezone ?? 'UTC'},
61
- command = ${manifest.workflow.command},
62
- run_spec = ${sql.json(manifest.workflow.run_spec ?? {})},
63
- updated_at = now()
64
- WHERE id = ${`sched-${manifest.name}`}
65
- `.catch(() => {});
66
- }
67
- break;
68
- }
69
- }
70
-
71
- // ============================================================================
72
- // Update handler
73
- // ============================================================================
74
-
75
- interface UpdateOptions {
76
- all?: boolean;
77
- }
78
-
79
- async function handleUpdateSingle(name: string, version?: string): Promise<boolean> {
80
- const existing = await getItemFromStore(name).catch(() => null);
81
- if (!existing) {
82
- console.error(`Item "${name}" is not installed.`);
83
- return false;
84
- }
85
-
86
- const installDir = existing.install_path ?? join(ITEMS_DIR, name);
87
- if (!existsSync(installDir)) {
88
- console.error(`Install directory not found: ${installDir}`);
89
- return false;
90
- }
91
-
92
- // Pull latest or checkout specific version
93
- try {
94
- if (version) {
95
- execSync(`git fetch --tags && git checkout ${version}`, {
96
- cwd: installDir,
97
- stdio: 'pipe',
98
- timeout: 60_000,
99
- });
100
- } else {
101
- execSync('git pull --ff-only', {
102
- cwd: installDir,
103
- stdio: 'pipe',
104
- timeout: 60_000,
105
- });
106
- }
107
- } catch (err) {
108
- console.error(`Git update failed for "${name}": ${(err as Error).message}`);
109
- return false;
110
- }
111
-
112
- // Re-detect and validate manifest
113
- const detection = await detectManifest(installDir);
114
- if ('error' in detection) {
115
- console.error(`Manifest detection failed after update: ${detection.error}`);
116
- return false;
117
- }
118
-
119
- const { manifest } = detection;
120
- const validation = validateManifest(manifest, installDir);
121
- for (const w of validation.warnings) {
122
- console.log(` Warning: ${w}`);
123
- }
124
- if (!validation.valid) {
125
- console.error(`Validation failed after update:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}`);
126
- return false;
127
- }
128
-
129
- // Update app_store
130
- await updateItemInStore(name, {
131
- version: manifest.version,
132
- description: manifest.description,
133
- manifest: manifest as unknown as Record<string, unknown>,
134
- });
135
-
136
- // Type-specific re-registration
137
- await reregisterByType(manifest);
138
-
139
- // Audit
140
- recordAuditEvent('item', name, 'item_updated', getActor(), {
141
- type: manifest.type,
142
- version: manifest.version,
143
- previousVersion: existing.version,
144
- }).catch(() => {});
145
-
146
- console.log(`Updated ${manifest.type} "${name}" → v${manifest.version}`);
147
- return true;
148
- }
149
-
150
- async function handleUpdate(nameOrVersion: string | undefined, options: UpdateOptions): Promise<void> {
151
- if (options.all) {
152
- const items = await listItemsFromStore();
153
- const gitItems = items.filter((i) => i.git_url);
154
- if (gitItems.length === 0) {
155
- console.log('No git-installed items to update.');
156
- return;
157
- }
158
-
159
- console.log(`Updating ${gitItems.length} item(s)...`);
160
- let updated = 0;
161
- for (const item of gitItems) {
162
- const ok = await handleUpdateSingle(item.name);
163
- if (ok) updated++;
164
- }
165
- console.log(`\n${updated}/${gitItems.length} items updated successfully.`);
166
- return;
167
- }
168
-
169
- if (!nameOrVersion) {
170
- console.error('Usage: genie update <name>[@version] or genie update --all');
171
- process.exit(1);
172
- }
173
-
174
- // Parse name@version
175
- let name = nameOrVersion;
176
- let version: string | undefined;
177
- const atIdx = name.lastIndexOf('@');
178
- if (atIdx > 0) {
179
- version = name.slice(atIdx + 1);
180
- name = name.slice(0, atIdx);
181
- }
182
-
183
- const ok = await handleUpdateSingle(name, version);
184
- if (!ok) process.exit(1);
185
- }
186
-
187
- // ============================================================================
188
- // Command registration
189
- // ============================================================================
190
-
191
- export function registerItemUpdateCommand(parent: Command): void {
192
- parent
193
- .command('update [name]')
194
- .description('Update an installed item to the latest version or a specific tag')
195
- .option('--all', 'Update all git-installed items')
196
- .action(async (name: string | undefined, options: UpdateOptions) => {
197
- try {
198
- await handleUpdate(name, options);
199
- } catch (error) {
200
- const message = error instanceof Error ? error.message : String(error);
201
- console.error(`Error: ${message}`);
202
- process.exit(1);
203
- }
204
- });
205
- }
@@ -1,187 +0,0 @@
1
- /**
2
- * Publish Command — `genie publish`
3
- *
4
- * Publishes the current directory as a genie item. Must be run from a
5
- * directory containing a genie.yaml. Requires a pushed git tag matching
6
- * the manifest version.
7
- */
8
-
9
- import { execSync } from 'node:child_process';
10
- import type { Command } from 'commander';
11
- import { getItemFromStore, registerItemInStore, updateItemInStore } from '../lib/agent-cache.js';
12
- import { getActor, recordAuditEvent } from '../lib/audit.js';
13
- import { getConnection, isAvailable } from '../lib/db.js';
14
- import { detectManifest, validateManifest } from '../lib/manifest.js';
15
-
16
- // ============================================================================
17
- // Git tag verification
18
- // ============================================================================
19
-
20
- function getGitRemoteTags(cwd: string): string[] {
21
- try {
22
- const output = execSync('git tag --list --merged HEAD', {
23
- cwd,
24
- encoding: 'utf-8',
25
- stdio: ['pipe', 'pipe', 'pipe'],
26
- });
27
- return output.trim().split('\n').filter(Boolean);
28
- } catch {
29
- return [];
30
- }
31
- }
32
-
33
- function isTagPushed(tag: string, cwd: string): boolean {
34
- try {
35
- execSync(`git ls-remote --tags origin refs/tags/${tag}`, {
36
- cwd,
37
- encoding: 'utf-8',
38
- stdio: ['pipe', 'pipe', 'pipe'],
39
- });
40
- return true;
41
- } catch {
42
- return false;
43
- }
44
- }
45
-
46
- function getGitSha(cwd: string): string | null {
47
- try {
48
- return execSync('git rev-parse HEAD', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
49
- } catch {
50
- return null;
51
- }
52
- }
53
-
54
- // ============================================================================
55
- // Publish handler
56
- // ============================================================================
57
-
58
- async function handlePublish(): Promise<void> {
59
- const cwd = process.cwd();
60
-
61
- // Detect and validate manifest
62
- const detection = await detectManifest(cwd);
63
- if ('error' in detection) {
64
- console.error(`Cannot publish: ${detection.error}`);
65
- process.exit(1);
66
- }
67
-
68
- const { manifest, source } = detection;
69
- const validation = validateManifest(manifest, cwd);
70
- for (const w of validation.warnings) {
71
- console.log(` Warning: ${w}`);
72
- }
73
- if (!validation.valid) {
74
- console.error(`Validation failed:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}`);
75
- process.exit(1);
76
- }
77
-
78
- // Check for git tag matching version
79
- const expectedTag = `v${manifest.version}`;
80
- const tags = getGitRemoteTags(cwd);
81
- const hasTag = tags.includes(expectedTag) || tags.includes(manifest.version);
82
- if (!hasTag) {
83
- console.error(`No git tag "${expectedTag}" found. Create and push a tag first:`);
84
- console.error(` git tag ${expectedTag} && git push origin ${expectedTag}`);
85
- process.exit(1);
86
- }
87
-
88
- // Verify tag is pushed to remote
89
- const tagToPush = tags.includes(expectedTag) ? expectedTag : manifest.version;
90
- if (!isTagPushed(tagToPush, cwd)) {
91
- console.error(`Tag "${tagToPush}" exists locally but is not pushed to remote.`);
92
- console.error(` git push origin ${tagToPush}`);
93
- process.exit(1);
94
- }
95
-
96
- const gitSha = getGitSha(cwd);
97
-
98
- // UPSERT into app_store
99
- const existing = await getItemFromStore(manifest.name).catch(() => null);
100
- if (existing) {
101
- await updateItemInStore(manifest.name, {
102
- version: manifest.version,
103
- description: manifest.description,
104
- manifest: manifest as unknown as Record<string, unknown>,
105
- });
106
-
107
- // Update approval status
108
- if (await isAvailable()) {
109
- const sql = await getConnection();
110
- await sql`
111
- UPDATE app_store
112
- SET approval_status = 'pending', updated_at = now()
113
- WHERE name = ${manifest.name}
114
- `;
115
- }
116
- } else {
117
- await registerItemInStore({
118
- name: manifest.name,
119
- itemType: manifest.type,
120
- version: manifest.version,
121
- description: manifest.description,
122
- authorName: manifest.author?.name,
123
- authorUrl: manifest.author?.url,
124
- installPath: cwd,
125
- manifest: manifest as unknown as Record<string, unknown>,
126
- tags: manifest.tags,
127
- category: manifest.category,
128
- license: manifest.license,
129
- dependencies: manifest.dependencies,
130
- });
131
- }
132
-
133
- // Record version in app_versions
134
- if (await isAvailable()) {
135
- const sql = await getConnection();
136
- const storeItem = await getItemFromStore(manifest.name);
137
- if (storeItem) {
138
- await sql`
139
- INSERT INTO app_versions (app_store_id, version, git_tag, git_sha, manifest)
140
- VALUES (
141
- ${storeItem.id},
142
- ${manifest.version},
143
- ${tagToPush},
144
- ${gitSha},
145
- ${sql.json(manifest as unknown as Record<string, unknown>)}
146
- )
147
- ON CONFLICT (app_store_id, version) DO UPDATE SET
148
- git_sha = EXCLUDED.git_sha,
149
- manifest = EXCLUDED.manifest,
150
- published_at = now()
151
- `;
152
- }
153
- }
154
-
155
- // Audit
156
- recordAuditEvent('item', manifest.name, 'item_published', getActor(), {
157
- type: manifest.type,
158
- version: manifest.version,
159
- tag: tagToPush,
160
- sha: gitSha,
161
- manifestSource: source,
162
- }).catch(() => {});
163
-
164
- console.log(`\nPublished ${manifest.type} "${manifest.name}" v${manifest.version}`);
165
- console.log(` Tag: ${tagToPush}`);
166
- if (gitSha) console.log(` SHA: ${gitSha.slice(0, 8)}`);
167
- console.log(' Status: pending approval');
168
- }
169
-
170
- // ============================================================================
171
- // Command registration
172
- // ============================================================================
173
-
174
- export function registerPublishCommand(program: Command): void {
175
- program
176
- .command('publish')
177
- .description('Publish the current directory as a genie item (requires pushed git tag)')
178
- .action(async () => {
179
- try {
180
- await handlePublish();
181
- } catch (error) {
182
- const message = error instanceof Error ? error.message : String(error);
183
- console.error(`Error: ${message}`);
184
- process.exit(1);
185
- }
186
- });
187
- }