@iservu-inc/adf-cli 0.14.5 → 0.16.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.
@@ -0,0 +1,439 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const inquirer = require('inquirer');
6
+ const { detectArtifactType, getArtifactTypes, isValidType } = require('../utils/artifact-detector');
7
+
8
+ /**
9
+ * Import Command
10
+ * Import existing markdown documentation into ADF
11
+ */
12
+
13
+ /**
14
+ * Generate a session ID for imported artifacts
15
+ * @returns {string} - Session ID in format: {timestamp}_imported
16
+ */
17
+ function generateSessionId() {
18
+ const now = new Date();
19
+ const timestamp = now.toISOString()
20
+ .replace(/[-:]/g, '')
21
+ .replace('T', '_')
22
+ .split('.')[0];
23
+ return `${timestamp}_imported`;
24
+ }
25
+
26
+ /**
27
+ * Normalize markdown content with ADF metadata
28
+ * @param {string} content - Original content
29
+ * @param {string} type - Artifact type
30
+ * @param {string} sourcePath - Original source path
31
+ * @returns {string} - Normalized content
32
+ */
33
+ function normalizeContent(content, type, sourcePath) {
34
+ // Check if frontmatter already exists
35
+ const hasFrontmatter = content.startsWith('---');
36
+
37
+ const adfMetadata = {
38
+ adf_type: type,
39
+ adf_imported: true,
40
+ adf_source: sourcePath,
41
+ adf_imported_at: new Date().toISOString()
42
+ };
43
+
44
+ if (hasFrontmatter) {
45
+ // Insert ADF metadata into existing frontmatter
46
+ return content.replace(/^---\s*\n/, `---\n${formatYamlMetadata(adfMetadata)}`);
47
+ }
48
+
49
+ // Add new frontmatter
50
+ const frontmatter = `---\n${formatYamlMetadata(adfMetadata)}---\n\n`;
51
+ return frontmatter + content;
52
+ }
53
+
54
+ /**
55
+ * Format metadata as YAML
56
+ * @param {Object} metadata - Metadata object
57
+ * @returns {string} - YAML formatted string
58
+ */
59
+ function formatYamlMetadata(metadata) {
60
+ return Object.entries(metadata)
61
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
62
+ .join('\n') + '\n';
63
+ }
64
+
65
+ /**
66
+ * Create session directory structure
67
+ * @param {string} projectPath - Project root
68
+ * @param {string} sessionId - Session ID
69
+ * @returns {Object} - Paths object
70
+ */
71
+ async function createSessionStructure(projectPath, sessionId) {
72
+ const sessionsDir = path.join(projectPath, '.adf', 'sessions');
73
+ const sessionPath = path.join(sessionsDir, sessionId);
74
+ const outputsPath = path.join(sessionPath, 'outputs');
75
+ const customPath = path.join(outputsPath, 'custom');
76
+ const sourcesPath = path.join(sessionPath, 'sources');
77
+
78
+ await fs.ensureDir(outputsPath);
79
+ await fs.ensureDir(customPath);
80
+ await fs.ensureDir(sourcesPath);
81
+
82
+ return { sessionPath, outputsPath, customPath, sourcesPath };
83
+ }
84
+
85
+ /**
86
+ * Create session metadata
87
+ * @param {string} sessionPath - Session directory path
88
+ * @param {Array} sourceFiles - List of source files
89
+ * @param {Array} artifactTypes - List of artifact types
90
+ * @param {string} projectPath - Project root path
91
+ */
92
+ async function createSessionMetadata(sessionPath, sourceFiles, artifactTypes, projectPath) {
93
+ const metadata = {
94
+ framework: 'imported',
95
+ importedAt: new Date().toISOString(),
96
+ sourceFiles: sourceFiles.map(f => path.relative(projectPath, f)),
97
+ artifactTypes: [...new Set(artifactTypes)],
98
+ projectPath: projectPath
99
+ };
100
+
101
+ await fs.writeJson(path.join(sessionPath, '_metadata.json'), metadata, { spaces: 2 });
102
+
103
+ const progress = {
104
+ status: 'completed',
105
+ canResume: false,
106
+ importedArtifacts: sourceFiles.length,
107
+ completedBlocks: artifactTypes,
108
+ totalBlocks: artifactTypes.length,
109
+ totalQuestionsAnswered: 0,
110
+ lastUpdated: new Date().toISOString()
111
+ };
112
+
113
+ await fs.writeJson(path.join(sessionPath, '_progress.json'), progress, { spaces: 2 });
114
+ }
115
+
116
+ /**
117
+ * Resolve files from input arguments (handles globs and directories)
118
+ * @param {Array} inputs - File paths, directories, or glob patterns
119
+ * @returns {Promise<Array>} - Resolved file paths
120
+ */
121
+ async function resolveFiles(inputs) {
122
+ const files = [];
123
+
124
+ for (const input of inputs) {
125
+ const fullPath = path.resolve(input);
126
+
127
+ if (!await fs.pathExists(fullPath)) {
128
+ console.warn(chalk.yellow(`⚠️ File not found: ${input}`));
129
+ continue;
130
+ }
131
+
132
+ const stats = await fs.stat(fullPath);
133
+
134
+ if (stats.isDirectory()) {
135
+ // Scan directory for markdown files
136
+ const dirFiles = await fs.readdir(fullPath);
137
+ for (const file of dirFiles) {
138
+ if (file.endsWith('.md') || file.endsWith('.markdown')) {
139
+ files.push(path.join(fullPath, file));
140
+ }
141
+ }
142
+ } else if (stats.isFile() && (fullPath.endsWith('.md') || fullPath.endsWith('.markdown'))) {
143
+ files.push(fullPath);
144
+ } else {
145
+ console.warn(chalk.yellow(`⚠️ Skipping non-markdown file: ${input}`));
146
+ }
147
+ }
148
+
149
+ return files;
150
+ }
151
+
152
+ /**
153
+ * Interactive type selection for a file
154
+ * @param {string} filePath - File path
155
+ * @param {Object} detection - Auto-detection result
156
+ * @returns {Promise<Object>} - Final type selection
157
+ */
158
+ async function promptForType(filePath, detection) {
159
+ const types = getArtifactTypes();
160
+ const filename = path.basename(filePath);
161
+
162
+ console.log(chalk.cyan(`\n📄 ${filename}`));
163
+
164
+ if (detection.confidence > 0) {
165
+ console.log(chalk.gray(` Auto-detected: ${detection.config.name} (${detection.confidence}% confidence)`));
166
+ }
167
+
168
+ const choices = [
169
+ ...Object.entries(types).map(([key, config]) => ({
170
+ name: `${config.name} → ${config.outputFile}`,
171
+ value: key
172
+ })),
173
+ { name: chalk.yellow('Custom (specify name)'), value: 'custom' },
174
+ { name: chalk.gray('Skip this file'), value: 'skip' }
175
+ ];
176
+
177
+ // Pre-select detected type if high confidence
178
+ const defaultChoice = detection.confidence >= 60 ? detection.type : undefined;
179
+
180
+ const { selectedType } = await inquirer.prompt([
181
+ {
182
+ type: 'list',
183
+ name: 'selectedType',
184
+ message: `Select artifact type for ${filename}:`,
185
+ choices,
186
+ default: defaultChoice
187
+ }
188
+ ]);
189
+
190
+ if (selectedType === 'skip') {
191
+ return null;
192
+ }
193
+
194
+ if (selectedType === 'custom') {
195
+ const { customName } = await inquirer.prompt([
196
+ {
197
+ type: 'input',
198
+ name: 'customName',
199
+ message: 'Enter custom artifact name:',
200
+ default: filename.replace(/\.md$/, ''),
201
+ validate: input => input.trim().length > 0 || 'Name cannot be empty'
202
+ }
203
+ ]);
204
+
205
+ return {
206
+ type: 'custom',
207
+ customName: customName.trim(),
208
+ outputFile: `custom/${customName.trim().toLowerCase().replace(/\s+/g, '-')}.md`
209
+ };
210
+ }
211
+
212
+ return {
213
+ type: selectedType,
214
+ outputFile: types[selectedType].outputFile
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Main import function
220
+ * @param {Array} files - Files to import
221
+ * @param {Object} options - Command options
222
+ */
223
+ async function importArtifacts(files, options = {}) {
224
+ const cwd = process.cwd();
225
+ const adfDir = path.join(cwd, '.adf');
226
+
227
+ // Ensure .adf directory exists
228
+ if (!await fs.pathExists(adfDir)) {
229
+ console.log(chalk.yellow('\n⚠️ No .adf directory found. Creating one...'));
230
+ await fs.ensureDir(adfDir);
231
+ }
232
+
233
+ // Resolve all input files
234
+ const resolvedFiles = await resolveFiles(files);
235
+
236
+ if (resolvedFiles.length === 0) {
237
+ console.error(chalk.red('\n❌ No valid markdown files found to import.'));
238
+ console.log(chalk.yellow('Specify .md files, directories, or use glob patterns.\n'));
239
+ process.exit(1);
240
+ }
241
+
242
+ console.log(chalk.cyan.bold(`\n📥 Importing ${resolvedFiles.length} file(s)...\n`));
243
+
244
+ // Dry run mode
245
+ if (options.dryRun) {
246
+ console.log(chalk.yellow.bold('DRY RUN - No changes will be made\n'));
247
+ }
248
+
249
+ // Process each file
250
+ const imports = [];
251
+ const skipped = [];
252
+
253
+ for (const filePath of resolvedFiles) {
254
+ const detection = await detectArtifactType(filePath);
255
+ let finalType;
256
+
257
+ if (options.interactive) {
258
+ // Interactive mode - prompt for each file
259
+ finalType = await promptForType(filePath, detection);
260
+ if (!finalType) {
261
+ skipped.push(filePath);
262
+ continue;
263
+ }
264
+ } else if (options.type) {
265
+ // Explicit type provided via --type flag
266
+ if (!isValidType(options.type)) {
267
+ console.error(chalk.red(`\n❌ Invalid artifact type: ${options.type}`));
268
+ console.log(chalk.yellow('Valid types: prd, architecture, stories, tasks, specification, constitution, plan, prp, custom\n'));
269
+ process.exit(1);
270
+ }
271
+
272
+ if (options.type === 'custom') {
273
+ const customName = options.name || path.basename(filePath, '.md');
274
+ finalType = {
275
+ type: 'custom',
276
+ customName,
277
+ outputFile: `custom/${customName.toLowerCase().replace(/\s+/g, '-')}.md`
278
+ };
279
+ } else {
280
+ const types = getArtifactTypes();
281
+ finalType = {
282
+ type: options.type,
283
+ outputFile: types[options.type].outputFile
284
+ };
285
+ }
286
+ } else {
287
+ // Auto-detection mode
288
+ if (detection.confidence < 50) {
289
+ console.log(chalk.yellow(`⚠️ Low confidence (${detection.confidence}%) for ${path.basename(filePath)}`));
290
+ console.log(chalk.gray(` Detected as: ${detection.config.name}`));
291
+ console.log(chalk.gray(` Use --interactive or --type to specify manually\n`));
292
+ }
293
+
294
+ finalType = {
295
+ type: detection.type,
296
+ outputFile: detection.config.outputFile,
297
+ confidence: detection.confidence
298
+ };
299
+ }
300
+
301
+ imports.push({
302
+ sourcePath: filePath,
303
+ ...finalType
304
+ });
305
+
306
+ if (!options.dryRun) {
307
+ console.log(chalk.green(`✓ ${path.basename(filePath)}`));
308
+ console.log(chalk.gray(` → ${finalType.outputFile} (${finalType.type})`));
309
+ }
310
+ }
311
+
312
+ if (options.dryRun) {
313
+ console.log(chalk.cyan.bold('\nWould import:\n'));
314
+ for (const item of imports) {
315
+ console.log(chalk.green(` ${path.basename(item.sourcePath)}`));
316
+ console.log(chalk.gray(` → ${item.outputFile} (${item.type})`));
317
+ }
318
+ if (skipped.length > 0) {
319
+ console.log(chalk.yellow(`\nWould skip: ${skipped.length} file(s)\n`));
320
+ }
321
+ return;
322
+ }
323
+
324
+ if (imports.length === 0) {
325
+ console.log(chalk.yellow('\nNo files to import after processing.\n'));
326
+ return;
327
+ }
328
+
329
+ // Create session
330
+ const spinner = ora('Creating import session...').start();
331
+
332
+ try {
333
+ const sessionId = options.session === 'latest'
334
+ ? await findLatestSession(cwd)
335
+ : options.session || generateSessionId();
336
+
337
+ const { sessionPath, outputsPath, customPath, sourcesPath } = await createSessionStructure(cwd, sessionId);
338
+
339
+ // Copy and normalize files
340
+ const artifactTypes = [];
341
+
342
+ for (const item of imports) {
343
+ // Read source
344
+ const content = await fs.readFile(item.sourcePath, 'utf-8');
345
+
346
+ // Normalize content
347
+ const normalized = options.noNormalize
348
+ ? content
349
+ : normalizeContent(content, item.type, path.relative(cwd, item.sourcePath));
350
+
351
+ // Determine output path
352
+ let outputPath;
353
+ if (item.type === 'custom') {
354
+ outputPath = path.join(customPath, item.customName
355
+ ? `${item.customName.toLowerCase().replace(/\s+/g, '-')}.md`
356
+ : path.basename(item.sourcePath));
357
+ } else {
358
+ outputPath = path.join(outputsPath, item.outputFile);
359
+ }
360
+
361
+ // Write normalized content
362
+ await fs.ensureDir(path.dirname(outputPath));
363
+ await fs.writeFile(outputPath, normalized);
364
+
365
+ // Preserve original
366
+ const originalFilename = `original_${path.basename(item.sourcePath)}`;
367
+ await fs.copy(item.sourcePath, path.join(sourcesPath, originalFilename));
368
+
369
+ artifactTypes.push(item.type);
370
+ }
371
+
372
+ // Create metadata
373
+ await createSessionMetadata(
374
+ sessionPath,
375
+ imports.map(i => i.sourcePath),
376
+ artifactTypes,
377
+ cwd
378
+ );
379
+
380
+ spinner.succeed(`Created import session: ${chalk.cyan(sessionId)}`);
381
+
382
+ console.log(chalk.gray(`\n Session: .adf/sessions/${sessionId}/`));
383
+ console.log(chalk.gray(` Artifacts: ${imports.length}`));
384
+ console.log(chalk.gray(` Types: ${[...new Set(artifactTypes)].join(', ')}`));
385
+
386
+ if (skipped.length > 0) {
387
+ console.log(chalk.yellow(` Skipped: ${skipped.length} file(s)`));
388
+ }
389
+
390
+ console.log(chalk.green.bold('\n✓ Import complete!'));
391
+ console.log(chalk.gray(' Run "adf deploy <tool>" to deploy imported artifacts.\n'));
392
+
393
+ } catch (error) {
394
+ spinner.fail(`Import failed: ${error.message}`);
395
+ throw error;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Find the latest session ID
401
+ * @param {string} cwd - Current working directory
402
+ * @returns {Promise<string|null>} - Latest session ID or null
403
+ */
404
+ async function findLatestSession(cwd) {
405
+ const sessionsDir = path.join(cwd, '.adf', 'sessions');
406
+
407
+ if (!await fs.pathExists(sessionsDir)) {
408
+ return null;
409
+ }
410
+
411
+ const sessions = await fs.readdir(sessionsDir);
412
+ if (sessions.length === 0) {
413
+ return null;
414
+ }
415
+
416
+ // Sort by timestamp (descending)
417
+ sessions.sort().reverse();
418
+ return sessions[0];
419
+ }
420
+
421
+ /**
422
+ * Command handler
423
+ */
424
+ async function importCommand(files, options) {
425
+ if (!files || files.length === 0) {
426
+ console.error(chalk.red('\n❌ Error: Please specify files to import.'));
427
+ console.log(chalk.yellow('Usage: adf import <files...> [options]'));
428
+ console.log(chalk.gray('\nExamples:'));
429
+ console.log(chalk.gray(' adf import ./docs/PRD.md'));
430
+ console.log(chalk.gray(' adf import ./docs/*.md --interactive'));
431
+ console.log(chalk.gray(' adf import ./design.md --type architecture\n'));
432
+ process.exit(1);
433
+ }
434
+
435
+ await importArtifacts(files, options);
436
+ }
437
+
438
+ module.exports = importCommand;
439
+ module.exports.importArtifacts = importArtifacts;
@@ -159,10 +159,14 @@ async function init(options) {
159
159
  const interviewer = new Interviewer(existingSession.progress.framework || 'balanced', cwd, existingSession, aiConfig);
160
160
  const sessionPath = await interviewer.start();
161
161
 
162
- console.log(chalk.green.bold('\n✨ Requirements gathering complete!\n'));
163
- console.log(chalk.cyan(`📁 Session saved to: ${sessionPath}\n`));
164
-
165
- return;
162
+ if (sessionPath === 'back') {
163
+ // User requested to go back to main menu
164
+ // Fall through to existing content check
165
+ } else {
166
+ console.log(chalk.green.bold('\n✨ Requirements gathering complete!\n'));
167
+ console.log(chalk.cyan(`📁 Session saved to: ${sessionPath}\n`));
168
+ return;
169
+ }
166
170
  }
167
171
 
168
172
  // Check if already initialized with actual content