aman-intelligence 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.
Files changed (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/dist/bin/aman.d.ts +2 -0
  4. package/dist/bin/aman.js +165 -0
  5. package/dist/cli/global-install.d.ts +7 -0
  6. package/dist/cli/global-install.js +36 -0
  7. package/dist/cli/help-text.d.ts +1 -0
  8. package/dist/cli/help-text.js +62 -0
  9. package/dist/cli/version.d.ts +1 -0
  10. package/dist/cli/version.js +6 -0
  11. package/dist/commands/backup.d.ts +11 -0
  12. package/dist/commands/backup.js +262 -0
  13. package/dist/commands/browse.d.ts +11 -0
  14. package/dist/commands/browse.js +641 -0
  15. package/dist/commands/cache.d.ts +1 -0
  16. package/dist/commands/cache.js +38 -0
  17. package/dist/commands/config.d.ts +4 -0
  18. package/dist/commands/config.js +146 -0
  19. package/dist/commands/dashboard.d.ts +1 -0
  20. package/dist/commands/dashboard.js +1004 -0
  21. package/dist/commands/doctor.d.ts +4 -0
  22. package/dist/commands/doctor.js +54 -0
  23. package/dist/commands/export.d.ts +1 -0
  24. package/dist/commands/export.js +137 -0
  25. package/dist/commands/help.d.ts +1 -0
  26. package/dist/commands/help.js +47 -0
  27. package/dist/commands/import-wizard.d.ts +7 -0
  28. package/dist/commands/import-wizard.js +374 -0
  29. package/dist/commands/import.d.ts +9 -0
  30. package/dist/commands/import.js +351 -0
  31. package/dist/commands/info.d.ts +1 -0
  32. package/dist/commands/info.js +174 -0
  33. package/dist/commands/init.d.ts +20 -0
  34. package/dist/commands/init.js +146 -0
  35. package/dist/commands/install.d.ts +10 -0
  36. package/dist/commands/install.js +342 -0
  37. package/dist/commands/pack.d.ts +23 -0
  38. package/dist/commands/pack.js +331 -0
  39. package/dist/commands/registry.d.ts +6 -0
  40. package/dist/commands/registry.js +218 -0
  41. package/dist/commands/remove.d.ts +1 -0
  42. package/dist/commands/remove.js +76 -0
  43. package/dist/commands/search.d.ts +7 -0
  44. package/dist/commands/search.js +295 -0
  45. package/dist/commands/stack.d.ts +18 -0
  46. package/dist/commands/stack.js +327 -0
  47. package/dist/commands/sync.d.ts +9 -0
  48. package/dist/commands/sync.js +428 -0
  49. package/dist/commands/update.d.ts +1 -0
  50. package/dist/commands/update.js +97 -0
  51. package/dist/config/features.d.ts +2 -0
  52. package/dist/config/features.js +2 -0
  53. package/dist/config/index.d.ts +13 -0
  54. package/dist/config/index.js +80 -0
  55. package/dist/config/paths.d.ts +23 -0
  56. package/dist/config/paths.js +45 -0
  57. package/dist/import/adapters.d.ts +14 -0
  58. package/dist/import/adapters.js +580 -0
  59. package/dist/import/discovery.service.d.ts +8 -0
  60. package/dist/import/discovery.service.js +26 -0
  61. package/dist/import/import.service.d.ts +7 -0
  62. package/dist/import/import.service.js +259 -0
  63. package/dist/import/types.d.ts +71 -0
  64. package/dist/import/types.js +1 -0
  65. package/dist/import/utils.d.ts +36 -0
  66. package/dist/import/utils.js +428 -0
  67. package/dist/marketplace/cache.d.ts +18 -0
  68. package/dist/marketplace/cache.js +141 -0
  69. package/dist/marketplace/github-search.d.ts +17 -0
  70. package/dist/marketplace/github-search.js +268 -0
  71. package/dist/marketplace/install-from-candidate.d.ts +6 -0
  72. package/dist/marketplace/install-from-candidate.js +14 -0
  73. package/dist/marketplace/install.d.ts +15 -0
  74. package/dist/marketplace/install.js +54 -0
  75. package/dist/marketplace/metadata-validator.d.ts +8 -0
  76. package/dist/marketplace/metadata-validator.js +79 -0
  77. package/dist/marketplace/types.d.ts +34 -0
  78. package/dist/marketplace/types.js +1 -0
  79. package/dist/providers/local.provider.d.ts +9 -0
  80. package/dist/providers/local.provider.js +51 -0
  81. package/dist/providers/provider.interface.d.ts +7 -0
  82. package/dist/providers/provider.interface.js +1 -0
  83. package/dist/providers/registry.provider.d.ts +2 -0
  84. package/dist/providers/registry.provider.js +42 -0
  85. package/dist/providers/skills-sh.provider.d.ts +11 -0
  86. package/dist/providers/skills-sh.provider.js +56 -0
  87. package/dist/registry/adapter.interface.d.ts +16 -0
  88. package/dist/registry/adapter.interface.js +1 -0
  89. package/dist/registry/errors.d.ts +5 -0
  90. package/dist/registry/errors.js +8 -0
  91. package/dist/registry/filesystem-registry.adapter.d.ts +25 -0
  92. package/dist/registry/filesystem-registry.adapter.js +288 -0
  93. package/dist/registry/github-registry.adapter.d.ts +11 -0
  94. package/dist/registry/github-registry.adapter.js +32 -0
  95. package/dist/registry/index.d.ts +8 -0
  96. package/dist/registry/index.js +8 -0
  97. package/dist/registry/local-registry.adapter.d.ts +6 -0
  98. package/dist/registry/local-registry.adapter.js +9 -0
  99. package/dist/registry/registry.service.d.ts +44 -0
  100. package/dist/registry/registry.service.js +163 -0
  101. package/dist/registry/slug-utils.d.ts +12 -0
  102. package/dist/registry/slug-utils.js +51 -0
  103. package/dist/registry/types.d.ts +160 -0
  104. package/dist/registry/types.js +1 -0
  105. package/dist/services/asset.service.d.ts +12 -0
  106. package/dist/services/asset.service.js +142 -0
  107. package/dist/services/backup.service.d.ts +8 -0
  108. package/dist/services/backup.service.js +169 -0
  109. package/dist/services/classification.service.d.ts +31 -0
  110. package/dist/services/classification.service.js +271 -0
  111. package/dist/services/config.service.d.ts +9 -0
  112. package/dist/services/config.service.js +20 -0
  113. package/dist/services/doctor.service.d.ts +5 -0
  114. package/dist/services/doctor.service.js +186 -0
  115. package/dist/services/environment.service.d.ts +42 -0
  116. package/dist/services/environment.service.js +227 -0
  117. package/dist/services/github.service.d.ts +7 -0
  118. package/dist/services/github.service.js +42 -0
  119. package/dist/services/lock.service.d.ts +12 -0
  120. package/dist/services/lock.service.js +71 -0
  121. package/dist/services/marketplace.service.d.ts +40 -0
  122. package/dist/services/marketplace.service.js +225 -0
  123. package/dist/services/pack.service.d.ts +9 -0
  124. package/dist/services/pack.service.js +193 -0
  125. package/dist/services/stack.service.d.ts +9 -0
  126. package/dist/services/stack.service.js +94 -0
  127. package/dist/storage/asset-layout.d.ts +46 -0
  128. package/dist/storage/asset-layout.js +277 -0
  129. package/dist/storage/filesystem.d.ts +12 -0
  130. package/dist/storage/filesystem.js +113 -0
  131. package/dist/storage/scan-by-type.d.ts +2 -0
  132. package/dist/storage/scan-by-type.js +8 -0
  133. package/dist/storage/scanner.d.ts +11 -0
  134. package/dist/storage/scanner.js +188 -0
  135. package/dist/types/asset-metadata.d.ts +84 -0
  136. package/dist/types/asset-metadata.js +104 -0
  137. package/dist/types/index.d.ts +212 -0
  138. package/dist/types/index.js +1 -0
  139. package/dist/ui/animations/ErrorIndicator.d.ts +5 -0
  140. package/dist/ui/animations/ErrorIndicator.js +6 -0
  141. package/dist/ui/animations/GithubIndicator.d.ts +6 -0
  142. package/dist/ui/animations/GithubIndicator.js +9 -0
  143. package/dist/ui/animations/ProgressBar.d.ts +5 -0
  144. package/dist/ui/animations/ProgressBar.js +15 -0
  145. package/dist/ui/animations/Spinner.d.ts +5 -0
  146. package/dist/ui/animations/Spinner.js +21 -0
  147. package/dist/ui/animations/SuccessIndicator.d.ts +5 -0
  148. package/dist/ui/animations/SuccessIndicator.js +6 -0
  149. package/dist/ui/animations/SyncActivity.d.ts +5 -0
  150. package/dist/ui/animations/SyncActivity.js +21 -0
  151. package/dist/ui/animations/TransitionScreen.d.ts +7 -0
  152. package/dist/ui/animations/TransitionScreen.js +25 -0
  153. package/dist/ui/animations/useAnimationMode.d.ts +1 -0
  154. package/dist/ui/animations/useAnimationMode.js +16 -0
  155. package/dist/ui/assetDisplay.d.ts +19 -0
  156. package/dist/ui/assetDisplay.js +59 -0
  157. package/dist/ui/components/Confirm.d.ts +8 -0
  158. package/dist/ui/components/Confirm.js +14 -0
  159. package/dist/ui/components/CustomSelect.d.ts +19 -0
  160. package/dist/ui/components/CustomSelect.js +13 -0
  161. package/dist/ui/components/Header.d.ts +6 -0
  162. package/dist/ui/components/Header.js +9 -0
  163. package/dist/ui/components/HealthReport.d.ts +7 -0
  164. package/dist/ui/components/HealthReport.js +13 -0
  165. package/dist/ui/components/MarketplaceInstallConfirm.d.ts +19 -0
  166. package/dist/ui/components/MarketplaceInstallConfirm.js +23 -0
  167. package/dist/ui/components/Narrator.d.ts +9 -0
  168. package/dist/ui/components/Narrator.js +26 -0
  169. package/dist/ui/components/ScopePrompt.d.ts +8 -0
  170. package/dist/ui/components/ScopePrompt.js +23 -0
  171. package/dist/ui/components/TooSmallScreen.d.ts +8 -0
  172. package/dist/ui/components/TooSmallScreen.js +6 -0
  173. package/dist/ui/date.d.ts +2 -0
  174. package/dist/ui/date.js +33 -0
  175. package/dist/ui/layout.d.ts +23 -0
  176. package/dist/ui/layout.js +44 -0
  177. package/dist/ui/list-item.d.ts +12 -0
  178. package/dist/ui/list-item.js +1 -0
  179. package/dist/ui/marketplaceDisplay.d.ts +10 -0
  180. package/dist/ui/marketplaceDisplay.js +36 -0
  181. package/dist/ui/theme.d.ts +42 -0
  182. package/dist/ui/theme.js +47 -0
  183. package/dist/utils/asset-list-fields.d.ts +11 -0
  184. package/dist/utils/asset-list-fields.js +28 -0
  185. package/dist/utils/error-message.d.ts +2 -0
  186. package/dist/utils/error-message.js +6 -0
  187. package/dist/utils/integrity.d.ts +9 -0
  188. package/dist/utils/integrity.js +23 -0
  189. package/dist/utils/lock-migrate.d.ts +25 -0
  190. package/dist/utils/lock-migrate.js +93 -0
  191. package/dist/utils/mcp-local.d.ts +15 -0
  192. package/dist/utils/mcp-local.js +129 -0
  193. package/dist/utils/slug.d.ts +6 -0
  194. package/dist/utils/slug.js +13 -0
  195. package/dist/utils/stack-normalize.d.ts +3 -0
  196. package/dist/utils/stack-normalize.js +43 -0
  197. package/package.json +77 -0
@@ -0,0 +1,193 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import archiver from 'archiver';
4
+ import { createWriteStream } from 'fs';
5
+ import { spawnSync } from 'child_process';
6
+ import { exists, ensureDir, removeDir } from '../storage/filesystem.js';
7
+ import { environmentService } from './environment.service.js';
8
+ import { lockService } from './lock.service.js';
9
+ import { defaultSlugForName } from '../utils/slug.js';
10
+ import { randomUUID } from 'crypto';
11
+ import { assetDir, migrateScopeLayout, contentFilePath } from '../storage/asset-layout.js';
12
+ import { mcpRequiresLocalConfig } from '../utils/mcp-local.js';
13
+ export class PackService {
14
+ async create(name, packData, outputPath) {
15
+ await ensureDir(path.dirname(outputPath));
16
+ return new Promise((resolve, reject) => {
17
+ const output = createWriteStream(outputPath);
18
+ const archive = archiver('zip', { zlib: { level: 9 } });
19
+ output.on('close', () => resolve());
20
+ archive.on('error', (err) => reject(err));
21
+ archive.pipe(output);
22
+ // Add manifest
23
+ archive.append(JSON.stringify(packData, null, 2), { name: 'manifest.json' });
24
+ // Add skills
25
+ for (const skill of packData.skills) {
26
+ const skillPath = path.join(environmentService.getActiveEnvironmentDir(), 'skills', skill);
27
+ if (exists(skillPath)) {
28
+ archive.directory(skillPath, `skills/${skill}`);
29
+ }
30
+ }
31
+ const envDir = environmentService.getActiveEnvironmentDir();
32
+ const promptsRoot = path.join(envDir, 'prompts');
33
+ const mcpsRoot = path.join(envDir, 'mcps');
34
+ for (const prompt of packData.prompts) {
35
+ const promptDir = assetDir('prompt', promptsRoot, prompt);
36
+ if (exists(promptDir)) {
37
+ archive.directory(promptDir, `prompts/${prompt}`);
38
+ }
39
+ }
40
+ for (const mcp of packData.mcps) {
41
+ const mcpDirPath = assetDir('mcp', mcpsRoot, mcp);
42
+ if (exists(mcpDirPath)) {
43
+ archive.directory(mcpDirPath, `mcps/${mcp}`);
44
+ }
45
+ }
46
+ // Add stacks
47
+ for (const stack of packData.stacks) {
48
+ const stackPath = path.join(environmentService.getActiveEnvironmentDir(), 'stacks', `${stack}.json`);
49
+ if (exists(stackPath)) {
50
+ archive.file(stackPath, { name: `stacks/${stack}.json` });
51
+ }
52
+ }
53
+ archive.finalize();
54
+ });
55
+ }
56
+ async install(packPath, scope, onProgress) {
57
+ if (!exists(packPath))
58
+ throw new Error(`Pack not found: ${packPath}`);
59
+ const tempDir = path.join(os.tmpdir(), `aman-pack-${Date.now()}`);
60
+ await ensureDir(tempDir);
61
+ try {
62
+ this.extractPack(packPath, tempDir);
63
+ onProgress?.(25);
64
+ const files = await this.walk(tempDir);
65
+ const { isPathSafe, readJson, copyDir } = await import('../storage/filesystem.js');
66
+ for (const file of files) {
67
+ const safe = await isPathSafe(file, tempDir);
68
+ if (!safe) {
69
+ throw new Error('Security Exception: Path traversal escape attempt detected inside pack archive!');
70
+ }
71
+ }
72
+ onProgress?.(50);
73
+ const manifestPath = path.join(tempDir, 'manifest.json');
74
+ const manifest = await readJson(manifestPath);
75
+ if (!manifest)
76
+ throw new Error('Invalid pack manifest');
77
+ const targetBase = scope === 'global'
78
+ ? await environmentService.ensureActiveEnvironment()
79
+ : await environmentService.ensureProjectEnvironment();
80
+ // Copy skills
81
+ if (exists(path.join(tempDir, 'skills'))) {
82
+ await copyDir(path.join(tempDir, 'skills'), path.join(targetBase, 'skills'));
83
+ }
84
+ // Copy prompts
85
+ if (exists(path.join(tempDir, 'prompts'))) {
86
+ await copyDir(path.join(tempDir, 'prompts'), path.join(targetBase, 'prompts'));
87
+ }
88
+ // Copy mcps
89
+ if (exists(path.join(tempDir, 'mcps'))) {
90
+ await copyDir(path.join(tempDir, 'mcps'), path.join(targetBase, 'mcps'));
91
+ }
92
+ // Copy stacks
93
+ if (exists(path.join(tempDir, 'stacks'))) {
94
+ await copyDir(path.join(tempDir, 'stacks'), path.join(targetBase, 'stacks'));
95
+ }
96
+ await migrateScopeLayout(targetBase);
97
+ onProgress?.(80);
98
+ const packRef = manifest.slug ?? defaultSlugForName(manifest.name);
99
+ const packVersion = manifest.version ?? '1.0.0';
100
+ const installedAt = new Date().toISOString();
101
+ const placeholderChecksum = 'sha256:0000000000000000000000000000000000000000000000000000000000000000';
102
+ const addPackEntry = async (localName, type) => {
103
+ let requiresLocalConfig = false;
104
+ if (type === 'mcp') {
105
+ const mcpDir = assetDir('mcp', path.join(targetBase, 'mcps'), localName);
106
+ const mcpData = await readJson(contentFilePath(mcpDir, 'mcp'));
107
+ requiresLocalConfig = mcpData ? mcpRequiresLocalConfig(mcpData) : false;
108
+ }
109
+ await lockService.addEntry(scope, {
110
+ id: randomUUID(),
111
+ slug: defaultSlugForName(localName),
112
+ type,
113
+ localName,
114
+ version: packVersion,
115
+ integrity: { algorithm: 'sha256', checksum: placeholderChecksum },
116
+ source: { kind: 'pack', ref: `pack:${packRef}@${packVersion}` },
117
+ scope,
118
+ installedAt,
119
+ dependencies: [],
120
+ requiresLocalConfig,
121
+ });
122
+ };
123
+ for (const skill of manifest.skills || []) {
124
+ await addPackEntry(skill, 'skill');
125
+ }
126
+ for (const prompt of manifest.prompts || []) {
127
+ await addPackEntry(prompt, 'prompt');
128
+ }
129
+ for (const mcp of manifest.mcps || []) {
130
+ await addPackEntry(mcp, 'mcp');
131
+ }
132
+ onProgress?.(100);
133
+ }
134
+ finally {
135
+ await removeDir(tempDir);
136
+ }
137
+ }
138
+ async inspect(packPath) {
139
+ if (!exists(packPath))
140
+ throw new Error(`Pack not found: ${packPath}`);
141
+ // Simplistic inspect by extracting manifest
142
+ const tempDir = path.join(os.tmpdir(), `aman-pack-inspect-${Date.now()}`);
143
+ await ensureDir(tempDir);
144
+ try {
145
+ this.extractPack(packPath, tempDir);
146
+ const files = await this.walk(tempDir);
147
+ const { isPathSafe, readJson } = await import('../storage/filesystem.js');
148
+ for (const file of files) {
149
+ const safe = await isPathSafe(file, tempDir);
150
+ if (!safe) {
151
+ throw new Error('Security Exception: Path traversal escape attempt detected inside pack archive!');
152
+ }
153
+ }
154
+ const manifestPath = path.join(tempDir, 'manifest.json');
155
+ if (!exists(manifestPath))
156
+ throw new Error('Invalid pack: missing manifest.json');
157
+ const manifest = await readJson(manifestPath);
158
+ return manifest;
159
+ }
160
+ finally {
161
+ await removeDir(tempDir);
162
+ }
163
+ }
164
+ async walk(root) {
165
+ const fsPromises = (await import('fs')).promises;
166
+ const entries = await fsPromises.readdir(root, { withFileTypes: true });
167
+ const files = [];
168
+ for (const entry of entries) {
169
+ const fullPath = path.join(root, entry.name);
170
+ if (entry.isDirectory()) {
171
+ files.push(...await this.walk(fullPath));
172
+ }
173
+ else if (entry.isFile()) {
174
+ files.push(fullPath);
175
+ }
176
+ }
177
+ return files;
178
+ }
179
+ extractPack(packPath, tempDir) {
180
+ const result = process.platform === 'win32'
181
+ ? spawnSync('tar', ['-xf', packPath, '-C', tempDir], { stdio: 'pipe' })
182
+ : spawnSync('unzip', ['-o', packPath, '-d', tempDir], { stdio: 'pipe' });
183
+ if (result.status !== 0) {
184
+ const stderr = result.stderr?.toString() || '';
185
+ const stdout = result.stdout?.toString() || '';
186
+ if (stderr.includes('..') || stderr.includes('path') || stdout.includes('..')) {
187
+ throw new Error('Security Exception: Path traversal escape attempt detected inside pack archive!');
188
+ }
189
+ throw new Error('Could not extract pack.');
190
+ }
191
+ }
192
+ }
193
+ export const packService = new PackService();
@@ -0,0 +1,9 @@
1
+ import { Stack, Scope } from '../types/index.js';
2
+ export declare class StackService {
3
+ private getTargetDir;
4
+ create(scope: Scope, name: string, skills: string[], prompts: string[], mcps: string[], description?: string): Promise<void>;
5
+ update(scope: Scope, name: string, updates: Partial<Pick<Stack, 'skills' | 'prompts' | 'mcps' | 'description'>>): Promise<void>;
6
+ remove(scope: Scope, name: string): Promise<void>;
7
+ list(scope: Scope): Promise<Stack[]>;
8
+ }
9
+ export declare const stackService: StackService;
@@ -0,0 +1,94 @@
1
+ import { randomUUID } from 'crypto';
2
+ import path from 'path';
3
+ import { LOCAL_DIR } from '../config/paths.js';
4
+ import { writeJson, exists, ensureDir, readJson } from '../storage/filesystem.js';
5
+ import { scanStacks } from '../storage/scanner.js';
6
+ import { promises as fs } from 'fs';
7
+ import { environmentService } from './environment.service.js';
8
+ import { defaultSlugForName } from '../utils/slug.js';
9
+ function buildMembers(skills, prompts, mcps, version = '1.0.0') {
10
+ return {
11
+ skills: skills.map((localName) => ({
12
+ localName,
13
+ slug: defaultSlugForName(localName),
14
+ version,
15
+ })),
16
+ prompts: prompts.map((localName) => ({
17
+ localName,
18
+ slug: defaultSlugForName(localName),
19
+ version,
20
+ })),
21
+ mcps: mcps.map((localName) => ({
22
+ localName,
23
+ slug: defaultSlugForName(localName),
24
+ version,
25
+ })),
26
+ };
27
+ }
28
+ export class StackService {
29
+ getTargetDir(scope) {
30
+ return scope === 'global'
31
+ ? path.join(environmentService.getActiveEnvironmentDir(), 'stacks')
32
+ : path.join(LOCAL_DIR, 'stacks');
33
+ }
34
+ async create(scope, name, skills, prompts, mcps, description) {
35
+ const now = new Date().toISOString();
36
+ const stack = {
37
+ schemaVersion: 1,
38
+ recordType: 'stack',
39
+ id: randomUUID(),
40
+ slug: `@local/${name}`,
41
+ name,
42
+ description,
43
+ visibility: 'private',
44
+ members: buildMembers(skills, prompts, mcps),
45
+ skills,
46
+ prompts,
47
+ mcps,
48
+ packs: [],
49
+ deprecated: null,
50
+ createdAt: now,
51
+ updatedAt: now,
52
+ };
53
+ const targetDir = this.getTargetDir(scope);
54
+ await ensureDir(targetDir);
55
+ const destPath = path.join(targetDir, `${name}.json`);
56
+ await writeJson(destPath, stack);
57
+ }
58
+ async update(scope, name, updates) {
59
+ const targetDir = this.getTargetDir(scope);
60
+ const destPath = path.join(targetDir, `${name}.json`);
61
+ if (!exists(destPath)) {
62
+ throw new Error(`Stack "${name}" not found in scope "${scope}"`);
63
+ }
64
+ const stack = await readJson(destPath);
65
+ if (!stack) {
66
+ throw new Error(`Failed to read stack "${name}" in scope "${scope}"`);
67
+ }
68
+ const skills = updates.skills ?? stack.skills;
69
+ const prompts = updates.prompts ?? stack.prompts;
70
+ const mcps = updates.mcps ?? stack.mcps;
71
+ const updatedStack = {
72
+ ...stack,
73
+ ...updates,
74
+ skills,
75
+ prompts,
76
+ mcps,
77
+ members: buildMembers(skills, prompts, mcps),
78
+ updatedAt: new Date().toISOString(),
79
+ };
80
+ await writeJson(destPath, updatedStack);
81
+ }
82
+ async remove(scope, name) {
83
+ const targetDir = this.getTargetDir(scope);
84
+ const destPath = path.join(targetDir, `${name}.json`);
85
+ if (exists(destPath)) {
86
+ await fs.unlink(destPath);
87
+ }
88
+ }
89
+ async list(scope) {
90
+ const targetDir = this.getTargetDir(scope);
91
+ return await scanStacks(targetDir, scope);
92
+ }
93
+ }
94
+ export const stackService = new StackService();
@@ -0,0 +1,46 @@
1
+ import { AssetType } from '../types/index.js';
2
+ export declare const CONTENT_FILES: Record<AssetType, string>;
3
+ export declare function assetDir(type: AssetType, typeRoot: string, localName: string): string;
4
+ export declare function contentFilePath(assetDirectory: string, type: AssetType): string;
5
+ export declare function metadataFilePath(assetDirectory: string): string;
6
+ export declare function mcpLocalFilePath(assetDirectory: string): string;
7
+ export declare function isCanonicalAssetDir(type: AssetType, dirPath: string): boolean;
8
+ /** Flat prompt: prompts/{name}.md + optional prompts/{name}.md.metadata.json */
9
+ export declare function flatPromptPaths(typeRoot: string, localName: string): {
10
+ md: string;
11
+ metadata: string;
12
+ };
13
+ /** Flat MCP: mcps/{name}.json + optional mcps/{name}.json.metadata.json */
14
+ export declare function flatMcpPaths(typeRoot: string, localName: string): {
15
+ json: string;
16
+ metadata: string;
17
+ local: string;
18
+ };
19
+ export declare function detectPromptLayout(typeRoot: string, localName: string): 'canonical' | 'flat' | 'missing';
20
+ export declare function detectMcpLayout(typeRoot: string, localName: string): 'canonical' | 'flat' | 'missing';
21
+ /**
22
+ * Migrates flat prompt/MCP files into canonical directory layout.
23
+ * Preserves content bytes (checksum unchanged). Removes flat files after success.
24
+ */
25
+ export declare function migratePromptFlatToCanonical(typeRoot: string, localName: string): Promise<boolean>;
26
+ export declare function migrateMcpFlatToCanonical(typeRoot: string, localName: string): Promise<boolean>;
27
+ /** Scan type root for flat files and migrate each. Returns count migrated. */
28
+ export declare function migrateTypeRootLayout(type: 'prompt' | 'mcp', typeRoot: string): Promise<number>;
29
+ /** Migrate prompts/ and mcps/ under a scope root (.aman or global env). */
30
+ export declare function migrateScopeLayout(scopeRoot: string): Promise<{
31
+ prompts: number;
32
+ mcps: number;
33
+ }>;
34
+ export interface LayoutViolation {
35
+ type: AssetType;
36
+ localName: string;
37
+ issue: string;
38
+ }
39
+ /** Lists assets not in canonical directory layout (post-migration). */
40
+ export declare function findLayoutViolations(scopeRoot: string): Promise<LayoutViolation[]>;
41
+ /**
42
+ * Install content from src (file or directory) into canonical asset directory.
43
+ */
44
+ export declare function materializeAssetDirectory(type: AssetType, src: string, destDir: string): Promise<void>;
45
+ /** Resolve bundled/default source path (flat file or directory). */
46
+ export declare function resolveBundledSource(type: AssetType, bundledDir: string, localName: string): string;
@@ -0,0 +1,277 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { exists, ensureDir, readJson, writeJson, listDirs, listFiles } from './filesystem.js';
4
+ import { computeAssetChecksum } from '../utils/integrity.js';
5
+ import { normalizeAssetMetadata } from '../types/asset-metadata.js';
6
+ export const CONTENT_FILES = {
7
+ skill: 'SKILL.md',
8
+ prompt: 'PROMPT.md',
9
+ mcp: 'mcp.json',
10
+ };
11
+ export function assetDir(type, typeRoot, localName) {
12
+ return path.join(typeRoot, localName);
13
+ }
14
+ export function contentFilePath(assetDirectory, type) {
15
+ return path.join(assetDirectory, CONTENT_FILES[type]);
16
+ }
17
+ export function metadataFilePath(assetDirectory) {
18
+ return path.join(assetDirectory, 'metadata.json');
19
+ }
20
+ export function mcpLocalFilePath(assetDirectory) {
21
+ return path.join(assetDirectory, 'mcp.local.json');
22
+ }
23
+ export function isCanonicalAssetDir(type, dirPath) {
24
+ if (!exists(dirPath))
25
+ return false;
26
+ return exists(contentFilePath(dirPath, type)) && exists(metadataFilePath(dirPath));
27
+ }
28
+ /** Flat prompt: prompts/{name}.md + optional prompts/{name}.md.metadata.json */
29
+ export function flatPromptPaths(typeRoot, localName) {
30
+ return {
31
+ md: path.join(typeRoot, `${localName}.md`),
32
+ metadata: path.join(typeRoot, `${localName}.md.metadata.json`),
33
+ };
34
+ }
35
+ /** Flat MCP: mcps/{name}.json + optional mcps/{name}.json.metadata.json */
36
+ export function flatMcpPaths(typeRoot, localName) {
37
+ return {
38
+ json: path.join(typeRoot, `${localName}.json`),
39
+ metadata: path.join(typeRoot, `${localName}.json.metadata.json`),
40
+ local: path.join(typeRoot, `${localName}.local.json`),
41
+ };
42
+ }
43
+ export function detectPromptLayout(typeRoot, localName) {
44
+ const dir = assetDir('prompt', typeRoot, localName);
45
+ if (isCanonicalAssetDir('prompt', dir))
46
+ return 'canonical';
47
+ const flat = flatPromptPaths(typeRoot, localName);
48
+ if (exists(flat.md))
49
+ return 'flat';
50
+ return 'missing';
51
+ }
52
+ export function detectMcpLayout(typeRoot, localName) {
53
+ const dir = assetDir('mcp', typeRoot, localName);
54
+ if (isCanonicalAssetDir('mcp', dir))
55
+ return 'canonical';
56
+ const flat = flatMcpPaths(typeRoot, localName);
57
+ if (exists(flat.json))
58
+ return 'flat';
59
+ return 'missing';
60
+ }
61
+ async function readMetadataFromFlatOrDir(type, typeRoot, localName, layout) {
62
+ if (layout === 'canonical') {
63
+ return readJson(metadataFilePath(assetDir(type, typeRoot, localName)));
64
+ }
65
+ const flatMeta = type === 'prompt'
66
+ ? flatPromptPaths(typeRoot, localName).metadata
67
+ : flatMcpPaths(typeRoot, localName).metadata;
68
+ return readJson(flatMeta);
69
+ }
70
+ /**
71
+ * Migrates flat prompt/MCP files into canonical directory layout.
72
+ * Preserves content bytes (checksum unchanged). Removes flat files after success.
73
+ */
74
+ export async function migratePromptFlatToCanonical(typeRoot, localName) {
75
+ const layout = detectPromptLayout(typeRoot, localName);
76
+ if (layout !== 'flat')
77
+ return layout === 'canonical';
78
+ const flat = flatPromptPaths(typeRoot, localName);
79
+ const destDir = assetDir('prompt', typeRoot, localName);
80
+ await ensureDir(destDir);
81
+ await fs.copyFile(flat.md, contentFilePath(destDir, 'prompt'));
82
+ const existingMeta = await readMetadataFromFlatOrDir('prompt', typeRoot, localName, 'flat');
83
+ const metadata = existingMeta
84
+ ? normalizeAssetMetadata(existingMeta, 'prompt', localName)
85
+ : normalizeAssetMetadata({}, 'prompt', localName);
86
+ const checksum = await computeAssetChecksum('prompt', destDir);
87
+ metadata.integrity.checksum = checksum;
88
+ await writeJson(metadataFilePath(destDir), metadata);
89
+ await fs.unlink(flat.md);
90
+ if (exists(flat.metadata))
91
+ await fs.unlink(flat.metadata);
92
+ return true;
93
+ }
94
+ export async function migrateMcpFlatToCanonical(typeRoot, localName) {
95
+ const layout = detectMcpLayout(typeRoot, localName);
96
+ if (layout !== 'flat')
97
+ return layout === 'canonical';
98
+ const flat = flatMcpPaths(typeRoot, localName);
99
+ const destDir = assetDir('mcp', typeRoot, localName);
100
+ await ensureDir(destDir);
101
+ await fs.copyFile(flat.json, contentFilePath(destDir, 'mcp'));
102
+ if (exists(flat.local)) {
103
+ await fs.copyFile(flat.local, mcpLocalFilePath(destDir));
104
+ }
105
+ const existingMeta = await readMetadataFromFlatOrDir('mcp', typeRoot, localName, 'flat');
106
+ const metadata = existingMeta
107
+ ? normalizeAssetMetadata(existingMeta, 'mcp', localName)
108
+ : normalizeAssetMetadata({}, 'mcp', localName);
109
+ const checksum = await computeAssetChecksum('mcp', destDir);
110
+ metadata.integrity.checksum = checksum;
111
+ await writeJson(metadataFilePath(destDir), metadata);
112
+ await fs.unlink(flat.json);
113
+ if (exists(flat.metadata))
114
+ await fs.unlink(flat.metadata);
115
+ if (exists(flat.local))
116
+ await fs.unlink(flat.local);
117
+ return true;
118
+ }
119
+ /** Scan type root for flat files and migrate each. Returns count migrated. */
120
+ export async function migrateTypeRootLayout(type, typeRoot) {
121
+ if (!exists(typeRoot))
122
+ return 0;
123
+ let migrated = 0;
124
+ if (type === 'prompt') {
125
+ const flatFiles = await listFiles(typeRoot, '.md');
126
+ for (const file of flatFiles) {
127
+ if (file.endsWith('.metadata.json'))
128
+ continue;
129
+ const localName = file.replace(/\.md$/, '');
130
+ if (await migratePromptFlatToCanonical(typeRoot, localName))
131
+ migrated++;
132
+ }
133
+ }
134
+ else {
135
+ const flatFiles = await listFiles(typeRoot, '.json');
136
+ for (const file of flatFiles) {
137
+ if (file.endsWith('.metadata.json') || file.endsWith('.local.json'))
138
+ continue;
139
+ const localName = file.replace(/\.json$/, '');
140
+ if (await migrateMcpFlatToCanonical(typeRoot, localName))
141
+ migrated++;
142
+ }
143
+ }
144
+ return migrated;
145
+ }
146
+ /** Migrate prompts/ and mcps/ under a scope root (.aman or global env). */
147
+ export async function migrateScopeLayout(scopeRoot) {
148
+ const promptsRoot = path.join(scopeRoot, 'prompts');
149
+ const mcpsRoot = path.join(scopeRoot, 'mcps');
150
+ const result = {
151
+ prompts: await migrateTypeRootLayout('prompt', promptsRoot),
152
+ mcps: await migrateTypeRootLayout('mcp', mcpsRoot),
153
+ };
154
+ if (exists(mcpsRoot)) {
155
+ const { ensureMcpLocalGitignore } = await import('../utils/mcp-local.js');
156
+ await ensureMcpLocalGitignore(scopeRoot);
157
+ }
158
+ return result;
159
+ }
160
+ /** Lists assets not in canonical directory layout (post-migration). */
161
+ export async function findLayoutViolations(scopeRoot) {
162
+ const violations = [];
163
+ const skillsRoot = path.join(scopeRoot, 'skills');
164
+ if (exists(skillsRoot)) {
165
+ for (const name of await listDirs(skillsRoot)) {
166
+ const dir = assetDir('skill', skillsRoot, name);
167
+ if (!isCanonicalAssetDir('skill', dir)) {
168
+ violations.push({ type: 'skill', localName: name, issue: 'missing SKILL.md or metadata.json in asset directory' });
169
+ }
170
+ }
171
+ for (const file of await listFiles(skillsRoot, '.md')) {
172
+ violations.push({ type: 'skill', localName: file, issue: 'unexpected flat file in skills/' });
173
+ }
174
+ }
175
+ const promptsRoot = path.join(scopeRoot, 'prompts');
176
+ if (exists(promptsRoot)) {
177
+ for (const file of await listFiles(promptsRoot, '.md')) {
178
+ violations.push({
179
+ type: 'prompt',
180
+ localName: file.replace(/\.md$/, ''),
181
+ issue: 'flat prompt file (expected prompts/{name}/PROMPT.md)',
182
+ });
183
+ }
184
+ for (const name of await listDirs(promptsRoot)) {
185
+ const dir = assetDir('prompt', promptsRoot, name);
186
+ if (!isCanonicalAssetDir('prompt', dir)) {
187
+ violations.push({ type: 'prompt', localName: name, issue: 'prompt directory missing PROMPT.md or metadata.json' });
188
+ }
189
+ }
190
+ }
191
+ const mcpsRoot = path.join(scopeRoot, 'mcps');
192
+ if (exists(mcpsRoot)) {
193
+ for (const file of await listFiles(mcpsRoot, '.json')) {
194
+ if (file.endsWith('.metadata.json') || file.endsWith('.local.json')) {
195
+ violations.push({ type: 'mcp', localName: file, issue: 'flat MCP sidecar at mcps/ root (expected directory layout)' });
196
+ continue;
197
+ }
198
+ violations.push({
199
+ type: 'mcp',
200
+ localName: file.replace(/\.json$/, ''),
201
+ issue: 'flat mcp.json file (expected mcps/{name}/mcp.json)',
202
+ });
203
+ }
204
+ for (const name of await listDirs(mcpsRoot)) {
205
+ const dir = assetDir('mcp', mcpsRoot, name);
206
+ if (!isCanonicalAssetDir('mcp', dir)) {
207
+ violations.push({ type: 'mcp', localName: name, issue: 'MCP directory missing mcp.json or metadata.json' });
208
+ }
209
+ }
210
+ }
211
+ return violations;
212
+ }
213
+ /**
214
+ * Install content from src (file or directory) into canonical asset directory.
215
+ */
216
+ export async function materializeAssetDirectory(type, src, destDir) {
217
+ await ensureDir(destDir);
218
+ const contentDest = contentFilePath(destDir, type);
219
+ if (type === 'skill') {
220
+ if (exists(path.join(src, 'SKILL.md'))) {
221
+ await copyDirContents(src, destDir);
222
+ return;
223
+ }
224
+ throw new Error(`Invalid skill source: ${src}`);
225
+ }
226
+ if (type === 'prompt') {
227
+ const canonicalSrc = path.join(src, 'PROMPT.md');
228
+ if (exists(canonicalSrc)) {
229
+ await copyDirContents(src, destDir);
230
+ return;
231
+ }
232
+ if (src.endsWith('.md') && exists(src)) {
233
+ await fs.copyFile(src, contentDest);
234
+ return;
235
+ }
236
+ throw new Error(`Invalid prompt source: ${src}`);
237
+ }
238
+ const canonicalMcp = path.join(src, 'mcp.json');
239
+ if (exists(canonicalMcp)) {
240
+ await copyDirContents(src, destDir);
241
+ return;
242
+ }
243
+ if (src.endsWith('.json') && exists(src)) {
244
+ await fs.copyFile(src, contentDest);
245
+ return;
246
+ }
247
+ throw new Error(`Invalid MCP source: ${src}`);
248
+ }
249
+ async function copyDirContents(src, dest) {
250
+ const entries = await fs.readdir(src, { withFileTypes: true });
251
+ for (const entry of entries) {
252
+ const srcPath = path.join(src, entry.name);
253
+ const destPath = path.join(dest, entry.name);
254
+ if (entry.isDirectory()) {
255
+ await ensureDir(destPath);
256
+ await copyDirContents(srcPath, destPath);
257
+ }
258
+ else {
259
+ await fs.copyFile(srcPath, destPath);
260
+ }
261
+ }
262
+ }
263
+ /** Resolve bundled/default source path (flat file or directory). */
264
+ export function resolveBundledSource(type, bundledDir, localName) {
265
+ const dir = assetDir(type, bundledDir, localName);
266
+ if (type === 'skill' && exists(path.join(dir, 'SKILL.md')))
267
+ return dir;
268
+ if (type === 'prompt' && exists(path.join(dir, 'PROMPT.md')))
269
+ return dir;
270
+ if (type === 'mcp' && exists(path.join(dir, 'mcp.json')))
271
+ return dir;
272
+ if (type === 'skill')
273
+ return dir;
274
+ if (type === 'prompt')
275
+ return path.join(bundledDir, `${localName}.md`);
276
+ return path.join(bundledDir, `${localName}.json`);
277
+ }
@@ -0,0 +1,12 @@
1
+ export declare function ensureDir(dirPath: string): Promise<void>;
2
+ export declare function readJson<T>(filePath: string): Promise<T | null>;
3
+ export declare function writeJson<T>(filePath: string, data: T): Promise<void>;
4
+ /** Atomic JSON write (temp file + rename) to avoid corrupted half-writes. */
5
+ export declare function writeJsonAtomic<T>(filePath: string, data: T): Promise<void>;
6
+ export declare function copyDir(src: string, dest: string): Promise<void>;
7
+ export declare function removeDir(dirPath: string): Promise<void>;
8
+ export declare function listDirs(dirPath: string): Promise<string[]>;
9
+ export declare function listFiles(dirPath: string, ext?: string): Promise<string[]>;
10
+ export declare function exists(targetPath: string): boolean;
11
+ export declare function readFrontmatter(mdPath: string): Promise<Record<string, unknown> | null>;
12
+ export declare function isPathSafe(targetPath: string, rootDir: string): Promise<boolean>;