@celilo/cli 0.1.0
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 +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { copyFile, mkdir, readFile, readdir } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { eq } from 'drizzle-orm';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { getWellKnownCapability, isWellKnown } from '../capabilities/well-known';
|
|
8
|
+
import { log } from '../cli/prompts';
|
|
9
|
+
import { getModuleStoragePath } from '../config/paths';
|
|
10
|
+
import { type DbClient, getDb } from '../db/client';
|
|
11
|
+
import { capabilities, moduleIntegrity, modules } from '../db/schema';
|
|
12
|
+
import type { NewModule, NewModuleIntegrity } from '../db/schema';
|
|
13
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
14
|
+
import {
|
|
15
|
+
validateCapabilityNames,
|
|
16
|
+
validateDeriveFromSources,
|
|
17
|
+
validateHookContract,
|
|
18
|
+
validateManifest,
|
|
19
|
+
validateProvidesNoCrossCapabilityRefs,
|
|
20
|
+
validateVariableSources,
|
|
21
|
+
validateZoneRequirements,
|
|
22
|
+
} from '../manifest/validate';
|
|
23
|
+
import { parseJsonWithValidation } from '../validation/schemas';
|
|
24
|
+
import { cleanupTempDir, extractPackage, verifyPackageIntegrity } from './packaging/extract';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Module import options
|
|
28
|
+
*/
|
|
29
|
+
export interface ModuleImportOptions {
|
|
30
|
+
sourcePath: string;
|
|
31
|
+
targetBasePath?: string;
|
|
32
|
+
db?: DbClient;
|
|
33
|
+
flags?: Record<string, string | boolean>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Module import result
|
|
38
|
+
*/
|
|
39
|
+
export interface ModuleImportSuccess {
|
|
40
|
+
success: true;
|
|
41
|
+
moduleId: string;
|
|
42
|
+
targetPath: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ModuleImportError {
|
|
46
|
+
success: false;
|
|
47
|
+
error: string;
|
|
48
|
+
details?: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ModuleImportResult = ModuleImportSuccess | ModuleImportError;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get default target base path for module storage
|
|
55
|
+
* Uses platform-specific defaults with environment variable overrides
|
|
56
|
+
*/
|
|
57
|
+
function getDefaultTargetBase(): string {
|
|
58
|
+
return getModuleStoragePath();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate module directory structure
|
|
63
|
+
*
|
|
64
|
+
* Policy function (Rule 10.1) - validates input only, no side effects
|
|
65
|
+
*
|
|
66
|
+
* @param sourcePath - Path to module directory
|
|
67
|
+
* @returns Error message if invalid, null if valid
|
|
68
|
+
*/
|
|
69
|
+
export function validateModuleDirectory(sourcePath: string): string | null {
|
|
70
|
+
// Check directory exists
|
|
71
|
+
if (!existsSync(sourcePath)) {
|
|
72
|
+
return `Module directory does not exist: ${sourcePath}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check it's a directory
|
|
76
|
+
const stats = statSync(sourcePath);
|
|
77
|
+
if (!stats.isDirectory()) {
|
|
78
|
+
return `Path is not a directory: ${sourcePath}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check manifest.yml exists
|
|
82
|
+
const manifestPath = join(sourcePath, 'manifest.yml');
|
|
83
|
+
if (!existsSync(manifestPath)) {
|
|
84
|
+
return 'manifest.yml not found in module directory';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read and validate manifest from module directory
|
|
92
|
+
*
|
|
93
|
+
* Policy function - reads and validates, no database access
|
|
94
|
+
*
|
|
95
|
+
* @param sourcePath - Path to module directory
|
|
96
|
+
* @returns Validated manifest or error
|
|
97
|
+
*/
|
|
98
|
+
export async function readModuleManifest(
|
|
99
|
+
sourcePath: string,
|
|
100
|
+
): Promise<{ success: true; manifest: ModuleManifest } | { success: false; error: string }> {
|
|
101
|
+
const manifestPath = join(sourcePath, 'manifest.yml');
|
|
102
|
+
|
|
103
|
+
let yamlContent: string;
|
|
104
|
+
try {
|
|
105
|
+
yamlContent = await readFile(manifestPath, 'utf-8');
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: `Failed to read manifest.yml: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const validationResult = validateManifest(yamlContent);
|
|
114
|
+
|
|
115
|
+
if (!validationResult.success) {
|
|
116
|
+
const errorMessages = validationResult.errors.map((e) => `${e.path}: ${e.message}`).join(', ');
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: `Manifest validation failed: ${errorMessages}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const zoneValidation = validateZoneRequirements(validationResult.data);
|
|
124
|
+
if (zoneValidation) {
|
|
125
|
+
const errorMessages = zoneValidation.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: `Zone validation failed:\n${errorMessages}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const deriveCheck = validateDeriveFromSources(validationResult.data);
|
|
133
|
+
if (deriveCheck) {
|
|
134
|
+
const errorMessages = deriveCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: `Variable derive_from validation failed:\n${errorMessages}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const hookCheck = validateHookContract(validationResult.data);
|
|
142
|
+
if (hookCheck) {
|
|
143
|
+
const errorMessages = hookCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: `Hook contract validation failed:\n${errorMessages}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const crossCapCheck = validateProvidesNoCrossCapabilityRefs(validationResult.data);
|
|
151
|
+
if (crossCapCheck) {
|
|
152
|
+
const errorMessages = crossCapCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: `Capability data validation failed:\n${errorMessages}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const capNameCheck = validateCapabilityNames(validationResult.data);
|
|
160
|
+
if (capNameCheck) {
|
|
161
|
+
const errorMessages = capNameCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
error: `Capability name validation failed:\n${errorMessages}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const variableSourceCheck = validateVariableSources(validationResult.data);
|
|
169
|
+
if (variableSourceCheck) {
|
|
170
|
+
const errorMessages = variableSourceCheck.errors
|
|
171
|
+
.map((e) => `${e.path}: ${e.message}`)
|
|
172
|
+
.join('\n');
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
error: `Variable source validation failed:\n${errorMessages}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
manifest: validationResult.data,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Copy module files to target directory
|
|
187
|
+
*
|
|
188
|
+
* Execution function (Rule 10.1) - performs file I/O
|
|
189
|
+
*
|
|
190
|
+
* @param sourcePath - Source module directory
|
|
191
|
+
* @param targetPath - Target directory to copy to
|
|
192
|
+
*/
|
|
193
|
+
export async function copyModuleFiles(sourcePath: string, targetPath: string): Promise<void> {
|
|
194
|
+
// Create target directory
|
|
195
|
+
await mkdir(targetPath, { recursive: true });
|
|
196
|
+
|
|
197
|
+
// Get all files in source directory
|
|
198
|
+
async function copyRecursive(src: string, dest: string) {
|
|
199
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
200
|
+
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
const srcPath = join(src, entry.name);
|
|
203
|
+
const destPath = join(dest, entry.name);
|
|
204
|
+
|
|
205
|
+
// Skip metadata files
|
|
206
|
+
if (entry.name === 'checksums.json' || entry.name === 'signature.sig') {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Skip directories that shouldn't be copied into celilo's data store.
|
|
211
|
+
// node_modules inside scripts/ ARE kept — they contain the module's
|
|
212
|
+
// runtime deps (@celilo/capabilities etc.). Top-level node_modules
|
|
213
|
+
// (monorepo workspace deps, build tools) are skipped.
|
|
214
|
+
if (
|
|
215
|
+
entry.isDirectory() &&
|
|
216
|
+
(entry.name === '.git' || entry.name === '.next' || entry.name === '.cache')
|
|
217
|
+
) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
entry.isDirectory() &&
|
|
223
|
+
entry.name === 'node_modules' &&
|
|
224
|
+
!src.endsWith('/scripts') &&
|
|
225
|
+
!src.endsWith('\\scripts')
|
|
226
|
+
) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
await mkdir(destPath, { recursive: true });
|
|
232
|
+
await copyRecursive(srcPath, destPath);
|
|
233
|
+
} else if (entry.isFile()) {
|
|
234
|
+
await copyFile(srcPath, destPath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await copyRecursive(sourcePath, targetPath);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Insert module into database
|
|
244
|
+
*
|
|
245
|
+
* Execution function - performs database write
|
|
246
|
+
*
|
|
247
|
+
* @param manifest - Validated manifest
|
|
248
|
+
* @param targetPath - Target path where files are stored
|
|
249
|
+
* @param db - Database client (optional, for testing)
|
|
250
|
+
* @returns Inserted module record
|
|
251
|
+
*/
|
|
252
|
+
export async function insertModuleToDb(
|
|
253
|
+
manifest: ModuleManifest,
|
|
254
|
+
targetPath: string,
|
|
255
|
+
db = getDb(),
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const newModule: NewModule = {
|
|
258
|
+
id: manifest.id,
|
|
259
|
+
name: manifest.name,
|
|
260
|
+
version: manifest.version,
|
|
261
|
+
description: manifest.description,
|
|
262
|
+
sourcePath: targetPath,
|
|
263
|
+
manifestData: manifest as Record<string, unknown>,
|
|
264
|
+
state: 'IMPORTED',
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
db.insert(modules).values(newModule).run();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if module already exists in database
|
|
272
|
+
*
|
|
273
|
+
* @param moduleId - Module ID to check
|
|
274
|
+
* @param db - Database client (optional, for testing)
|
|
275
|
+
* @returns True if module exists
|
|
276
|
+
*/
|
|
277
|
+
export function moduleExists(moduleId: string, db = getDb()): boolean {
|
|
278
|
+
const existing = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
279
|
+
return existing !== undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Validate well-known capabilities
|
|
284
|
+
*
|
|
285
|
+
* Policy function - checks if module's well-known capabilities are valid:
|
|
286
|
+
* 1. No other module provides the same well-known capability (uniqueness)
|
|
287
|
+
* 2. Module's zone matches capability's required zone (zone enforcement)
|
|
288
|
+
*
|
|
289
|
+
* @param manifest - Module manifest
|
|
290
|
+
* @param db - Database client
|
|
291
|
+
* @returns Error message if invalid, null if valid
|
|
292
|
+
*/
|
|
293
|
+
export async function validateWellKnownCapabilities(
|
|
294
|
+
manifest: ModuleManifest,
|
|
295
|
+
db = getDb(),
|
|
296
|
+
): Promise<string | null> {
|
|
297
|
+
const providedCapabilities = manifest.provides?.capabilities ?? [];
|
|
298
|
+
|
|
299
|
+
for (const capability of providedCapabilities) {
|
|
300
|
+
// Skip non-well-known capabilities
|
|
301
|
+
if (!isWellKnown(capability.name)) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const wellKnown = getWellKnownCapability(capability.name);
|
|
306
|
+
|
|
307
|
+
// Check 1: Capability uniqueness - only one module can provide this capability
|
|
308
|
+
const existingCapability = await db
|
|
309
|
+
.select()
|
|
310
|
+
.from(capabilities)
|
|
311
|
+
.where(eq(capabilities.capabilityName, capability.name))
|
|
312
|
+
.all();
|
|
313
|
+
|
|
314
|
+
if (existingCapability.length > 0) {
|
|
315
|
+
const conflictingModule = existingCapability[0];
|
|
316
|
+
return `Well-known capability '${capability.name}' is already provided by module '${conflictingModule.moduleId}'. A home lab can only have one module providing this capability. Remove '${conflictingModule.moduleId}' before importing this module.`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check 2: Zone enforcement - module must be in the correct zone
|
|
320
|
+
const moduleZone = manifest.requires?.machine?.zone;
|
|
321
|
+
|
|
322
|
+
if (wellKnown.zone_enforced && moduleZone) {
|
|
323
|
+
if (moduleZone !== wellKnown.required_zone) {
|
|
324
|
+
return `Capability '${capability.name}' requires zone='${wellKnown.required_zone}' (security requirement). Module manifest specifies zone='${moduleZone}'. Update the module manifest to use the correct zone.`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Import a module from a directory or .netapp package
|
|
334
|
+
*
|
|
335
|
+
* Orchestration function (Rule 10.1) - coordinates policy, planning, and execution
|
|
336
|
+
* This is the main entry point for module import
|
|
337
|
+
*
|
|
338
|
+
* @param options - Import options
|
|
339
|
+
* @returns Import result
|
|
340
|
+
*/
|
|
341
|
+
/**
|
|
342
|
+
* The minimum package.json auto-generated for modules that have hook
|
|
343
|
+
* scripts but no package.json. Contains only the framework dep.
|
|
344
|
+
*/
|
|
345
|
+
const DEFAULT_SCRIPTS_PACKAGE_JSON = JSON.stringify(
|
|
346
|
+
{
|
|
347
|
+
private: true,
|
|
348
|
+
dependencies: {
|
|
349
|
+
'@celilo/capabilities': '^0.1.0',
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
null,
|
|
353
|
+
2,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Install npm dependencies for a module's hook scripts.
|
|
358
|
+
*
|
|
359
|
+
* Per NPM_PACKAGE_RESOLUTION design doc (Option B): each module's
|
|
360
|
+
* scripts/ directory has its own package.json + node_modules for
|
|
361
|
+
* fully isolated dependency resolution. This function:
|
|
362
|
+
*
|
|
363
|
+
* 1. Finds the scripts/ directory (derived from manifest hook paths).
|
|
364
|
+
* 2. If package.json exists and node_modules/ is missing, runs
|
|
365
|
+
* `bun install`.
|
|
366
|
+
* 3. If no package.json exists but hook scripts do, auto-generates a
|
|
367
|
+
* default one with `@celilo/capabilities` and then installs.
|
|
368
|
+
* 4. If no hooks are declared, does nothing.
|
|
369
|
+
*/
|
|
370
|
+
async function installScriptDependencies(
|
|
371
|
+
targetPath: string,
|
|
372
|
+
manifest: ModuleManifest,
|
|
373
|
+
): Promise<void> {
|
|
374
|
+
// Determine where scripts live by looking at hook declarations.
|
|
375
|
+
// All hooks use paths like `./scripts/setup-admin.ts`, so the
|
|
376
|
+
// scripts directory is the common parent.
|
|
377
|
+
if (!manifest.hooks || Object.keys(manifest.hooks).length === 0) {
|
|
378
|
+
return; // No hooks → no scripts → nothing to install
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const scriptsDir = join(targetPath, 'scripts');
|
|
382
|
+
if (!existsSync(scriptsDir)) {
|
|
383
|
+
return; // No scripts directory on disk
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const pkgJsonPath = join(scriptsDir, 'package.json');
|
|
387
|
+
const nodeModulesPath = join(scriptsDir, 'node_modules');
|
|
388
|
+
|
|
389
|
+
// Auto-generate package.json if missing but hooks exist
|
|
390
|
+
if (!existsSync(pkgJsonPath)) {
|
|
391
|
+
log.info('Auto-generating scripts/package.json with @celilo/capabilities');
|
|
392
|
+
writeFileSync(pkgJsonPath, DEFAULT_SCRIPTS_PACKAGE_JSON, 'utf-8');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Skip install if node_modules already exists (pre-bundled via
|
|
396
|
+
// future --bundle-deps, or a re-import of an already-installed module)
|
|
397
|
+
if (existsSync(nodeModulesPath)) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Run bun install in the scripts directory
|
|
402
|
+
log.info('Installing script dependencies...');
|
|
403
|
+
execSync('bun install', {
|
|
404
|
+
cwd: scriptsDir,
|
|
405
|
+
timeout: 120_000,
|
|
406
|
+
stdio: 'pipe',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function importModule(options: ModuleImportOptions): Promise<ModuleImportResult> {
|
|
411
|
+
const { sourcePath, targetBasePath = getDefaultTargetBase(), db = getDb(), flags = {} } = options;
|
|
412
|
+
|
|
413
|
+
let actualSourcePath = sourcePath;
|
|
414
|
+
let tempDir: string | null = null;
|
|
415
|
+
let checksums: Record<string, string> | null = null;
|
|
416
|
+
let signature: string | null = null;
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
// Check if source is a .netapp package
|
|
420
|
+
if (sourcePath.endsWith('.netapp')) {
|
|
421
|
+
// Extract and verify package
|
|
422
|
+
const extractResult = await extractPackage(sourcePath);
|
|
423
|
+
if (!extractResult.success || !extractResult.tempDir) {
|
|
424
|
+
return { success: false, error: extractResult.error || 'Failed to extract package' };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
tempDir = extractResult.tempDir;
|
|
428
|
+
|
|
429
|
+
// Verify package integrity (unless --skip-verify)
|
|
430
|
+
const skipVerify = flags['skip-verify'] === true;
|
|
431
|
+
if (skipVerify) {
|
|
432
|
+
log.warn('Skipping package signature verification (--skip-verify)');
|
|
433
|
+
} else {
|
|
434
|
+
const verifyResult = await verifyPackageIntegrity(tempDir);
|
|
435
|
+
if (!verifyResult.success) {
|
|
436
|
+
await cleanupTempDir(tempDir);
|
|
437
|
+
return {
|
|
438
|
+
success: false,
|
|
439
|
+
error: verifyResult.error || 'Package integrity verification failed',
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Read checksums and signature for database storage
|
|
445
|
+
const checksumsJson = await readFile(join(tempDir, 'checksums.json'), 'utf-8');
|
|
446
|
+
const ChecksumsFileSchema = z.object({
|
|
447
|
+
files: z.record(z.string(), z.string()),
|
|
448
|
+
});
|
|
449
|
+
const checksumsData = parseJsonWithValidation(
|
|
450
|
+
checksumsJson,
|
|
451
|
+
ChecksumsFileSchema,
|
|
452
|
+
'package checksums.json',
|
|
453
|
+
);
|
|
454
|
+
checksums = checksumsData.files;
|
|
455
|
+
signature = await readFile(join(tempDir, 'signature.sig'), 'utf-8');
|
|
456
|
+
|
|
457
|
+
// Use extracted directory as source
|
|
458
|
+
actualSourcePath = tempDir;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Policy: Validate directory structure
|
|
462
|
+
const dirError = validateModuleDirectory(actualSourcePath);
|
|
463
|
+
if (dirError) {
|
|
464
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
465
|
+
return { success: false, error: dirError };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Policy: Read and validate manifest
|
|
469
|
+
const manifestResult = await readModuleManifest(actualSourcePath);
|
|
470
|
+
if (!manifestResult.success) {
|
|
471
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
472
|
+
return { success: false, error: manifestResult.error };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const manifest = manifestResult.manifest;
|
|
476
|
+
|
|
477
|
+
// Policy: Check if module already exists
|
|
478
|
+
if (moduleExists(manifest.id, db)) {
|
|
479
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
480
|
+
return {
|
|
481
|
+
success: false,
|
|
482
|
+
error: `Module '${manifest.id}' already exists. Use update or remove it first.`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Policy: Validate well-known capabilities (import-time check)
|
|
487
|
+
const capabilityError = await validateWellKnownCapabilities(manifest, db);
|
|
488
|
+
if (capabilityError) {
|
|
489
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
490
|
+
return {
|
|
491
|
+
success: false,
|
|
492
|
+
error: capabilityError,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Policy: Validate template variable references (import-time check)
|
|
497
|
+
const { validateModuleTemplates, formatTemplateValidationErrors } = await import(
|
|
498
|
+
'../manifest/template-validator'
|
|
499
|
+
);
|
|
500
|
+
const templateValidation = await validateModuleTemplates(actualSourcePath, manifest);
|
|
501
|
+
if (!templateValidation.success) {
|
|
502
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
503
|
+
return {
|
|
504
|
+
success: false,
|
|
505
|
+
error: formatTemplateValidationErrors(templateValidation.errors),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Policy: Check for Ansible dependency conflicts and install collections
|
|
510
|
+
const { installCollectionsForModule } = await import('../ansible/dependencies');
|
|
511
|
+
const installResult = await installCollectionsForModule(manifest, db);
|
|
512
|
+
|
|
513
|
+
if (!installResult.success) {
|
|
514
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
515
|
+
return {
|
|
516
|
+
success: false,
|
|
517
|
+
error: installResult.error || 'Failed to install Ansible collections',
|
|
518
|
+
details: installResult.details,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Execution: Validate capability access if module requires capabilities
|
|
523
|
+
if (manifest.requires?.capabilities && manifest.requires.capabilities.length > 0) {
|
|
524
|
+
const { validateCapabilityAccess } = await import('../capabilities/validation');
|
|
525
|
+
const accessResult = await validateCapabilityAccess(manifest, db.$client);
|
|
526
|
+
|
|
527
|
+
if (!accessResult.success) {
|
|
528
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
529
|
+
return {
|
|
530
|
+
success: false,
|
|
531
|
+
error: accessResult.error || 'Capability access denied',
|
|
532
|
+
details: accessResult.details,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Planning: Determine target path
|
|
538
|
+
const targetPath = join(targetBasePath, manifest.id);
|
|
539
|
+
|
|
540
|
+
// Execution: Copy files
|
|
541
|
+
try {
|
|
542
|
+
await copyModuleFiles(actualSourcePath, targetPath);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
545
|
+
return {
|
|
546
|
+
success: false,
|
|
547
|
+
error: 'Failed to copy module files',
|
|
548
|
+
details: error,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Execution: Install hook script dependencies (NPM_PACKAGE_RESOLUTION Option B).
|
|
553
|
+
// If the module's scripts/ directory has a package.json, run `bun install`
|
|
554
|
+
// so the hook scripts can resolve their npm imports (e.g. @celilo/capabilities).
|
|
555
|
+
// If no package.json exists but hook scripts do, auto-generate one with
|
|
556
|
+
// just the framework dep — smooths migration for existing modules.
|
|
557
|
+
try {
|
|
558
|
+
await installScriptDependencies(targetPath, manifest);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
// Non-fatal: the module is importable without deps, but hooks
|
|
561
|
+
// will fail at runtime. Warn and continue.
|
|
562
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
563
|
+
log.warn(`Failed to install script dependencies: ${msg}`);
|
|
564
|
+
log.warn('Hook scripts may fail to resolve imports until this is fixed.');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Execution: Insert to database
|
|
568
|
+
try {
|
|
569
|
+
await insertModuleToDb(manifest, targetPath, db);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
572
|
+
return {
|
|
573
|
+
success: false,
|
|
574
|
+
error: 'Failed to insert module to database',
|
|
575
|
+
details: error,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Execution: Register capabilities if module provides them
|
|
580
|
+
if (manifest.provides?.capabilities && manifest.provides.capabilities.length > 0) {
|
|
581
|
+
const { registerModuleCapabilities } = await import('../capabilities/registration');
|
|
582
|
+
const capResult = await registerModuleCapabilities(manifest.id, manifest, db.$client, flags);
|
|
583
|
+
|
|
584
|
+
if (!capResult.success) {
|
|
585
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
586
|
+
return {
|
|
587
|
+
success: false,
|
|
588
|
+
error: capResult.error || 'Failed to register module capabilities',
|
|
589
|
+
details: capResult.details,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Execution: Store integrity data
|
|
595
|
+
// For .netapp packages: store checksums + signature
|
|
596
|
+
// For directory imports: calculate and store checksums (no signature)
|
|
597
|
+
try {
|
|
598
|
+
let finalChecksums: Record<string, string>;
|
|
599
|
+
let finalSignature: string | null = null;
|
|
600
|
+
|
|
601
|
+
if (checksums && signature) {
|
|
602
|
+
// From .netapp package - already verified
|
|
603
|
+
finalChecksums = checksums;
|
|
604
|
+
finalSignature = signature.trim();
|
|
605
|
+
} else {
|
|
606
|
+
// From directory - calculate checksums now
|
|
607
|
+
const { computeChecksums } = await import('./packaging/build');
|
|
608
|
+
const checksumsData = await computeChecksums(targetPath);
|
|
609
|
+
finalChecksums = checksumsData.files;
|
|
610
|
+
// No signature for directory imports
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const integrityData: NewModuleIntegrity = {
|
|
614
|
+
moduleId: manifest.id,
|
|
615
|
+
checksums: finalChecksums,
|
|
616
|
+
signature: finalSignature,
|
|
617
|
+
};
|
|
618
|
+
db.insert(moduleIntegrity).values(integrityData).run();
|
|
619
|
+
} catch (error) {
|
|
620
|
+
// Non-fatal - module is already imported
|
|
621
|
+
console.warn('Warning: Failed to store module integrity data', error);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// For .netapp packages with build artifacts: record a successful build
|
|
625
|
+
// so that `module generate` doesn't require a rebuild
|
|
626
|
+
if (sourcePath.endsWith('.netapp') && manifest.build?.artifacts) {
|
|
627
|
+
const { existsSync: fileExists } = await import('node:fs');
|
|
628
|
+
const artifactPaths = manifest.build.artifacts
|
|
629
|
+
.map((a: string) => join(targetPath, a))
|
|
630
|
+
.filter((p: string) => fileExists(p));
|
|
631
|
+
|
|
632
|
+
if (artifactPaths.length > 0) {
|
|
633
|
+
const { moduleBuilds } = await import('../db/schema');
|
|
634
|
+
db.insert(moduleBuilds)
|
|
635
|
+
.values({
|
|
636
|
+
moduleId: manifest.id,
|
|
637
|
+
version: manifest.version,
|
|
638
|
+
artifacts: artifactPaths,
|
|
639
|
+
status: 'success',
|
|
640
|
+
buildLog: 'Pre-built artifacts from .netapp package',
|
|
641
|
+
})
|
|
642
|
+
.run();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Update dependency cache
|
|
647
|
+
try {
|
|
648
|
+
const { updateDependencyCache } = await import('../ansible/dependencies');
|
|
649
|
+
await updateDependencyCache(db);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
// Non-fatal - cache is optional
|
|
652
|
+
console.warn('Warning: Failed to update dependency cache', error);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Cleanup temp directory if used
|
|
656
|
+
if (tempDir) {
|
|
657
|
+
await cleanupTempDir(tempDir);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
success: true,
|
|
662
|
+
moduleId: manifest.id,
|
|
663
|
+
targetPath,
|
|
664
|
+
};
|
|
665
|
+
} catch (error) {
|
|
666
|
+
// Cleanup on unexpected error
|
|
667
|
+
if (tempDir) {
|
|
668
|
+
await cleanupTempDir(tempDir);
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
success: false,
|
|
672
|
+
error: 'Unexpected error during import',
|
|
673
|
+
details: error,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|