@vectorasystems/cli 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorasystems/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Vectora CLI — AI-powered project orchestration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,47 @@ import { getConfig, getConfigValue } from '../lib/config-store.js';
7
7
  import { handleError } from '../lib/errors.js';
8
8
  import { renderTable, renderJson, renderTime, warn, info, success } from '../lib/output.js';
9
9
 
10
+ function extensionForContentType(contentType) {
11
+ const normalized = String(contentType || '').toLowerCase();
12
+ if (normalized.includes('markdown')) return 'md';
13
+ if (normalized.includes('json')) return 'json';
14
+ if (normalized.startsWith('text/')) return 'txt';
15
+ return 'bin';
16
+ }
17
+
18
+ function stripKnownExtensions(filename) {
19
+ let base = String(filename || '').trim();
20
+ while (/\.(md|markdown|json|txt|text|bin)$/i.test(base)) {
21
+ base = base.replace(/\.(md|markdown|json|txt|text|bin)$/i, '');
22
+ }
23
+ return base;
24
+ }
25
+
26
+ function fallbackFilename(artifact) {
27
+ const extension = extensionForContentType(artifact?.contentType);
28
+ if (artifact?.type === 'prd') return `PRD.${extension}`;
29
+ if (artifact?.type === 'roadmap-markdown') return `ROADMAP.${extension}`;
30
+ if (artifact?.type === 'validation-report-markdown') return `VALIDATION_REPORT.${extension}`;
31
+ if (artifact?.type === 'architecture-map') return `ARCHITECTURE_MAP.${extension}`;
32
+ if (artifact?.type === 'handoff-generic') return `VECTORA.${extension}`;
33
+ if (String(artifact?.type || '').startsWith('handoff-')) {
34
+ const target = artifact.type.slice('handoff-'.length).replace(/[^a-z0-9]+/gi, '_').toUpperCase();
35
+ return `VECTORA_${target}.${extension}`;
36
+ }
37
+ return `${artifact?.type || 'artifact'}-v${artifact?.version ?? 1}.${extension}`;
38
+ }
39
+
40
+ function normalizeFilename(filename, artifact) {
41
+ const extension = extensionForContentType(artifact?.contentType);
42
+ const fallback = fallbackFilename(artifact);
43
+ const trimmed = String(filename || '').trim();
44
+ if (!trimmed) return fallback;
45
+
46
+ const base = stripKnownExtensions(trimmed);
47
+ if (!base) return fallback;
48
+ return `${base}.${extension}`;
49
+ }
50
+
10
51
  /**
11
52
  * vectora artifacts list [--project <id>] [--type <type>]
12
53
  */
@@ -100,10 +141,10 @@ export async function download(id, opts) {
100
141
  return;
101
142
  }
102
143
 
144
+ const artifact = await getArtifact(id);
103
145
  // Get presigned URL + suggested filename from API
104
146
  const { url, filename, inlineBase64 } = await getArtifactDownloadUrl(id);
105
-
106
- const outPath = resolve(opts.out ?? filename);
147
+ const outPath = resolve(opts.out ?? normalizeFilename(filename, artifact));
107
148
 
108
149
  // Local-storage fallback: API returns the file bytes inline when no
109
150
  // presigned URL backend is configured.
@@ -48,15 +48,34 @@ export async function* parseSseStream(body) {
48
48
  * @yields {{ event: string, data: object }}
49
49
  */
50
50
  export async function* streamPhaseProgress(baseUrl, jobId, token) {
51
- const res = await fetch(`${baseUrl}/v1/phases/${jobId}?token=${token}`, {
52
- signal: AbortSignal.timeout(5 * 60 * 1000), // 5 min
53
- });
51
+ while (true) {
52
+ const res = await fetch(`${baseUrl}/v1/phases/${jobId}?token=${token}`);
54
53
 
55
- if (!res.ok || !res.body) {
56
- throw new Error(`Phase SSE failed: HTTP ${res.status}`);
57
- }
54
+ if (!res.ok || !res.body) {
55
+ throw new Error(`Phase SSE failed: HTTP ${res.status}`);
56
+ }
58
57
 
59
- yield* parseSseStream(res.body);
58
+ let reconnect = false;
59
+
60
+ for await (const frame of parseSseStream(res.body)) {
61
+ if (frame.event === 'job:timeout') {
62
+ reconnect = true;
63
+ break;
64
+ }
65
+
66
+ yield frame;
67
+
68
+ if (frame.event === 'job:completed' || frame.event === 'job:failed') {
69
+ return;
70
+ }
71
+ }
72
+
73
+ if (!reconnect) {
74
+ return;
75
+ }
76
+
77
+ await new Promise((resolve) => setTimeout(resolve, 1000));
78
+ }
60
79
  }
61
80
 
62
81
  /**
@@ -7,15 +7,46 @@ import { writeFile } from 'node:fs/promises';
7
7
  import { resolve } from 'node:path';
8
8
  import { getProjectArtifacts, getArtifactDownloadUrl } from '../../lib/api-client.js';
9
9
 
10
- const TYPE_FILENAME = {
11
- handoff: 'VECTORA.md',
12
- prd: 'PRD.md',
13
- scope: 'SCOPE.md',
14
- roadmap: 'ROADMAP.md',
15
- analysis: 'ANALYSIS.json',
16
- validation: 'VALIDATION.json',
17
- feedback: 'FEEDBACK.json',
18
- };
10
+ function extensionForContentType(contentType) {
11
+ const normalized = String(contentType || '').toLowerCase();
12
+ if (normalized.includes('markdown')) return 'md';
13
+ if (normalized.includes('json')) return 'json';
14
+ if (normalized.startsWith('text/')) return 'txt';
15
+ return 'bin';
16
+ }
17
+
18
+ function stripKnownExtensions(filename) {
19
+ let base = String(filename || '').trim();
20
+ while (/\.(md|markdown|json|txt|text|bin)$/i.test(base)) {
21
+ base = base.replace(/\.(md|markdown|json|txt|text|bin)$/i, '');
22
+ }
23
+ return base;
24
+ }
25
+
26
+ function fallbackFilename(artifact) {
27
+ const extension = extensionForContentType(artifact?.contentType);
28
+ if (artifact?.type === 'prd') return `PRD.${extension}`;
29
+ if (artifact?.type === 'roadmap-markdown') return `ROADMAP.${extension}`;
30
+ if (artifact?.type === 'validation-report-markdown') return `VALIDATION_REPORT.${extension}`;
31
+ if (artifact?.type === 'architecture-map') return `ARCHITECTURE_MAP.${extension}`;
32
+ if (artifact?.type === 'handoff-generic') return `VECTORA.${extension}`;
33
+ if (String(artifact?.type || '').startsWith('handoff-')) {
34
+ const target = artifact.type.slice('handoff-'.length).replace(/[^a-z0-9]+/gi, '_').toUpperCase();
35
+ return `VECTORA_${target}.${extension}`;
36
+ }
37
+ return `${artifact?.type || 'artifact'}-v${artifact?.version ?? 1}.${extension}`;
38
+ }
39
+
40
+ function normalizeFilename(filename, artifact) {
41
+ const extension = extensionForContentType(artifact?.contentType);
42
+ const fallback = fallbackFilename(artifact);
43
+ const trimmed = String(filename || '').trim();
44
+ if (!trimmed) return fallback;
45
+
46
+ const base = stripKnownExtensions(trimmed);
47
+ if (!base) return fallback;
48
+ return `${base}.${extension}`;
49
+ }
19
50
 
20
51
  function formatDate(iso) {
21
52
  return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
@@ -43,8 +74,9 @@ export function ArtifactBrowser({ projectId, onBack }) {
43
74
  setSavedPath(null);
44
75
  setError(null);
45
76
  try {
77
+ const artifact = artifacts.find((entry) => entry.id === item.value);
46
78
  const { url, filename, inlineBase64 } = await getArtifactDownloadUrl(item.value);
47
- const outPath = resolve(filename);
79
+ const outPath = resolve(normalizeFilename(filename, artifact));
48
80
 
49
81
  // Local-storage fallback: the API can return inline bytes when no
50
82
  // presigned URL backend is available.