vigthoria-cli 1.13.20 → 1.13.23
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/completions/_vigthoria +49 -0
- package/completions/vigthoria.bash +6 -0
- package/completions/vigthoria.fish +41 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +63 -128
- package/dist/commands/background.d.ts +0 -2
- package/dist/commands/background.js +26 -34
- package/dist/commands/bridge.js +45 -41
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +22 -16
- package/dist/commands/chat.d.ts +3 -42
- package/dist/commands/chat.js +378 -1123
- package/dist/commands/coding-registration.d.ts +4 -0
- package/dist/commands/coding-registration.js +24 -0
- package/dist/commands/config.js +23 -29
- package/dist/commands/conversation-registration.d.ts +4 -0
- package/dist/commands/conversation-registration.js +122 -0
- package/dist/commands/deploy.d.ts +8 -2
- package/dist/commands/deploy.js +92 -90
- package/dist/commands/device.d.ts +1 -2
- package/dist/commands/device.js +37 -21
- package/dist/commands/doctor-registration.d.ts +4 -0
- package/dist/commands/doctor-registration.js +207 -0
- package/dist/commands/edit.js +22 -33
- package/dist/commands/explain.js +7 -10
- package/dist/commands/fork.d.ts +1 -2
- package/dist/commands/fork.js +72 -22
- package/dist/commands/game.d.ts +1 -1
- package/dist/commands/game.js +17 -6
- package/dist/commands/generate.js +7 -9
- package/dist/commands/history.js +11 -7
- package/dist/commands/hub.d.ts +2 -2
- package/dist/commands/hub.js +34 -35
- package/dist/commands/legion.d.ts +0 -1
- package/dist/commands/legion.js +62 -79
- package/dist/commands/music.d.ts +1 -2
- package/dist/commands/music.js +62 -27
- package/dist/commands/platform-registration.d.ts +4 -0
- package/dist/commands/platform-registration.js +45 -0
- package/dist/commands/preview.d.ts +0 -1
- package/dist/commands/preview.js +82 -35
- package/dist/commands/product-run-registration.d.ts +4 -0
- package/dist/commands/product-run-registration.js +490 -0
- package/dist/commands/replay.d.ts +1 -2
- package/dist/commands/replay.js +15 -11
- package/dist/commands/repo.d.ts +14 -7
- package/dist/commands/repo.js +244 -198
- package/dist/commands/review.js +9 -12
- package/dist/commands/security.d.ts +1 -2
- package/dist/commands/security.js +12 -6
- package/dist/commands/update-registration.d.ts +3 -0
- package/dist/commands/update-registration.js +368 -0
- package/dist/commands/v4-registration.d.ts +4 -0
- package/dist/commands/v4-registration.js +24 -0
- package/dist/commands/v4.d.ts +1 -1
- package/dist/commands/v4.js +46 -8
- package/dist/commands/wallet.d.ts +1 -3
- package/dist/commands/wallet.js +18 -45
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +52 -39
- package/dist/index.js +124 -1589
- package/dist/utils/agent-stream-state.d.ts +27 -0
- package/dist/utils/agent-stream-state.js +93 -0
- package/dist/utils/agentRoute.d.ts +2 -1
- package/dist/utils/agentRoute.js +22 -9
- package/dist/utils/agentRunOutcome.d.ts +16 -0
- package/dist/utils/agentRunOutcome.js +37 -2
- package/dist/utils/api-client-factory.d.ts +8 -0
- package/dist/utils/api-client-factory.js +8 -0
- package/dist/utils/api.d.ts +44 -266
- package/dist/utils/api.js +647 -3180
- package/dist/utils/auth-session.d.ts +41 -0
- package/dist/utils/auth-session.js +224 -0
- package/dist/utils/brain-hub-client.d.ts +0 -4
- package/dist/utils/brain-hub-client.js +6 -3
- package/dist/utils/bridge-client.js +2 -0
- package/dist/utils/capability-health-service.d.ts +48 -0
- package/dist/utils/capability-health-service.js +64 -0
- package/dist/utils/chat-prompt-policy.d.ts +18 -0
- package/dist/utils/chat-prompt-policy.js +61 -0
- package/dist/utils/chat-tool-call-parser.d.ts +11 -0
- package/dist/utils/chat-tool-call-parser.js +71 -0
- package/dist/utils/cli-state.d.ts +22 -4
- package/dist/utils/cli-state.js +105 -47
- package/dist/utils/code-operations-service.d.ts +104 -0
- package/dist/utils/code-operations-service.js +837 -0
- package/dist/utils/codebase-indexer.d.ts +1 -0
- package/dist/utils/codebase-indexer.js +28 -8
- package/dist/utils/command-contract.d.ts +61 -0
- package/dist/utils/command-contract.js +149 -0
- package/dist/utils/command-policy.d.ts +10 -0
- package/dist/utils/command-policy.js +54 -0
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +220 -94
- package/dist/utils/context-ranker.js +11 -2
- package/dist/utils/context-state.d.ts +39 -0
- package/dist/utils/context-state.js +52 -0
- package/dist/utils/deckEvents.d.ts +0 -5
- package/dist/utils/deckEvents.js +3 -2
- package/dist/utils/desktop-bridge-client.js +2 -0
- package/dist/utils/direct-output-policy.d.ts +3 -0
- package/dist/utils/direct-output-policy.js +76 -0
- package/dist/utils/durable-json.d.ts +57 -0
- package/dist/utils/durable-json.js +319 -0
- package/dist/utils/fastAgentRouter.d.ts +5 -6
- package/dist/utils/fastAgentRouter.js +17 -25
- package/dist/utils/files.js +21 -13
- package/dist/utils/frontend-preview-service.d.ts +76 -0
- package/dist/utils/frontend-preview-service.js +482 -0
- package/dist/utils/goaEvents.d.ts +0 -4
- package/dist/utils/goaEvents.js +5 -2
- package/dist/utils/logger.d.ts +2 -1
- package/dist/utils/logger.js +27 -20
- package/dist/utils/mcp-context-client.d.ts +24 -0
- package/dist/utils/mcp-context-client.js +58 -0
- package/dist/utils/model-governance.d.ts +18 -0
- package/dist/utils/model-governance.js +75 -0
- package/dist/utils/model-transport-service.d.ts +44 -0
- package/dist/utils/model-transport-service.js +111 -0
- package/dist/utils/mutation-journal.d.ts +42 -0
- package/dist/utils/mutation-journal.js +387 -0
- package/dist/utils/network-policy.d.ts +24 -0
- package/dist/utils/network-policy.js +202 -0
- package/dist/utils/operator-client.d.ts +45 -0
- package/dist/utils/operator-client.js +159 -0
- package/dist/utils/post-write-validator.js +7 -9
- package/dist/utils/preview-screenshot-adapter.d.ts +15 -0
- package/dist/utils/preview-screenshot-adapter.js +51 -0
- package/dist/utils/process-policy.d.ts +23 -0
- package/dist/utils/process-policy.js +284 -0
- package/dist/utils/project-memory.d.ts +13 -3
- package/dist/utils/project-memory.js +70 -17
- package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
- package/dist/utils/prompt-workspace-resolver.js +64 -0
- package/dist/utils/release-install.d.ts +11 -0
- package/dist/utils/release-install.js +155 -0
- package/dist/utils/release-policy.d.ts +64 -0
- package/dist/utils/release-policy.js +293 -0
- package/dist/utils/requestIntent.js +22 -3
- package/dist/utils/runtime-capability.d.ts +8 -0
- package/dist/utils/runtime-capability.js +16 -0
- package/dist/utils/runtime-temp.d.ts +76 -0
- package/dist/utils/runtime-temp.js +492 -0
- package/dist/utils/secret-policy.d.ts +20 -0
- package/dist/utils/secret-policy.js +160 -0
- package/dist/utils/session.d.ts +17 -3
- package/dist/utils/session.js +354 -110
- package/dist/utils/tool-approval-service.d.ts +17 -0
- package/dist/utils/tool-approval-service.js +78 -0
- package/dist/utils/tool-capability-providers.d.ts +57 -0
- package/dist/utils/tool-capability-providers.js +57 -0
- package/dist/utils/tools.d.ts +2 -23
- package/dist/utils/tools.js +226 -512
- package/dist/utils/update-policy.d.ts +18 -0
- package/dist/utils/update-policy.js +137 -0
- package/dist/utils/v3-agent-client.d.ts +40 -0
- package/dist/utils/v3-agent-client.js +165 -0
- package/dist/utils/v3-workspace-service.d.ts +28 -0
- package/dist/utils/v3-workspace-service.js +279 -0
- package/dist/utils/vigflow-client.d.ts +95 -0
- package/dist/utils/vigflow-client.js +105 -0
- package/dist/utils/workspace-boundary.d.ts +45 -0
- package/dist/utils/workspace-boundary.js +377 -0
- package/dist/utils/workspace-brain-service.d.ts +2 -0
- package/dist/utils/workspace-brain-service.js +11 -1
- package/dist/utils/workspace-cache.js +37 -15
- package/dist/utils/workspace-stream.js +35 -10
- package/install.ps1 +115 -142
- package/install.sh +112 -81
- package/package.json +43 -9
- package/release-policy.json +43 -0
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
- package/scripts/release/generate-completions.mjs +24 -0
- package/scripts/release/install-release.mjs +10 -0
- package/scripts/release/publish-cli-release.mjs +116 -0
- package/scripts/release/publish-cli-release.sh +2 -70
- package/scripts/release/resolve-release-manifest.mjs +26 -0
- package/scripts/release/test-balanced-model-live.sh +45 -0
- package/scripts/release/validate-live-service-gates.sh +93 -0
- package/scripts/release/validate-no-go-gates.sh +145 -174
- package/scripts/release/verify-runtime-consistency.mjs +4 -1
- package/dist/commands/index.d.ts +0 -12
- package/dist/commands/index.js +0 -194
- package/dist/commands/update.d.ts +0 -9
- package/dist/commands/update.js +0 -301
- package/dist/utils/templateInstantPath.d.ts +0 -18
- package/dist/utils/templateInstantPath.js +0 -228
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { createHash } from 'node:crypto';
|
|
11
|
+
import { redactSensitiveText } from './secret-policy.js';
|
|
12
|
+
import { DurableJsonStore } from './durable-json.js';
|
|
11
13
|
const MAX_ITEMS = 240;
|
|
12
14
|
const MAX_TEXT_LENGTH = 320;
|
|
13
15
|
const MAX_CONTEXT_ITEMS = 14;
|
|
16
|
+
const MAX_BRAIN_BYTES = 512 * 1024;
|
|
14
17
|
const STOP_WORDS = new Set([
|
|
15
18
|
'about', 'after', 'again', 'also', 'because', 'before', 'could', 'from', 'have', 'into', 'just', 'make', 'need',
|
|
16
19
|
'only', 'please', 'should', 'that', 'their', 'there', 'this', 'with', 'work', 'working', 'would', 'your', 'vigthoria',
|
|
@@ -19,7 +22,7 @@ function nowIso() {
|
|
|
19
22
|
return new Date().toISOString();
|
|
20
23
|
}
|
|
21
24
|
function normalizeText(value) {
|
|
22
|
-
return String(value || '')
|
|
25
|
+
return redactSensitiveText(String(value || ''))
|
|
23
26
|
.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, ' ')
|
|
24
27
|
.replace(/[ \t\r\n]+/g, ' ')
|
|
25
28
|
.trim();
|
|
@@ -63,43 +66,75 @@ export class ProjectMemoryService {
|
|
|
63
66
|
workspacePath;
|
|
64
67
|
memoryDir;
|
|
65
68
|
brainPath;
|
|
66
|
-
|
|
69
|
+
brainStore;
|
|
70
|
+
enabled;
|
|
71
|
+
constructor(workspacePath = process.cwd(), options = {}) {
|
|
67
72
|
this.workspacePath = path.resolve(workspacePath || process.cwd());
|
|
68
73
|
this.memoryDir = path.join(this.workspacePath, '.vigthoria', 'memory');
|
|
69
74
|
this.brainPath = path.join(this.memoryDir, 'brain.json');
|
|
75
|
+
const configured = String(process.env.VIGTHORIA_PROJECT_MEMORY || 'local-only').trim().toLowerCase();
|
|
76
|
+
this.enabled = options.enabled ?? !['0', 'off', 'false', 'disabled'].includes(configured);
|
|
77
|
+
this.brainStore = new DurableJsonStore({
|
|
78
|
+
filePath: this.brainPath,
|
|
79
|
+
schemaVersion: 2,
|
|
80
|
+
defaultValue: () => this.emptyBrain(),
|
|
81
|
+
validate: (value) => Boolean(value && typeof value === 'object'
|
|
82
|
+
&& value.version === 2
|
|
83
|
+
&& value.privacyMode === 'local-only'
|
|
84
|
+
&& Array.isArray(value.items)
|
|
85
|
+
&& Buffer.byteLength(JSON.stringify(value), 'utf8') <= MAX_BRAIN_BYTES),
|
|
86
|
+
migrate: (value) => {
|
|
87
|
+
if (!value || typeof value !== 'object' || !Array.isArray(value.items))
|
|
88
|
+
return this.emptyBrain();
|
|
89
|
+
const legacy = value;
|
|
90
|
+
return {
|
|
91
|
+
...legacy,
|
|
92
|
+
version: 2,
|
|
93
|
+
privacyMode: 'local-only',
|
|
94
|
+
workspaceName: legacy.workspaceName || path.basename(this.workspacePath),
|
|
95
|
+
updatedAt: legacy.updatedAt || nowIso(),
|
|
96
|
+
items: this.dedupeAndTrim(legacy.items),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
70
100
|
}
|
|
71
101
|
getMemoryDir() {
|
|
72
102
|
return this.memoryDir;
|
|
73
103
|
}
|
|
74
104
|
loadBrain() {
|
|
105
|
+
if (!this.enabled)
|
|
106
|
+
return this.emptyBrain();
|
|
75
107
|
try {
|
|
76
|
-
|
|
77
|
-
const parsed = JSON.parse(fs.readFileSync(this.brainPath, 'utf8'));
|
|
78
|
-
if (parsed && parsed.version === 1 && Array.isArray(parsed.items)) {
|
|
79
|
-
return parsed;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
108
|
+
return this.brainStore.read();
|
|
82
109
|
}
|
|
83
110
|
catch {
|
|
84
111
|
// Corrupt memory should never break chat.
|
|
85
112
|
}
|
|
113
|
+
return this.emptyBrain();
|
|
114
|
+
}
|
|
115
|
+
emptyBrain() {
|
|
86
116
|
return {
|
|
87
|
-
version:
|
|
117
|
+
version: 2,
|
|
88
118
|
workspaceName: path.basename(this.workspacePath),
|
|
89
119
|
updatedAt: nowIso(),
|
|
120
|
+
privacyMode: 'local-only',
|
|
90
121
|
items: [],
|
|
91
122
|
};
|
|
92
123
|
}
|
|
93
124
|
saveBrain(brain) {
|
|
125
|
+
if (!this.enabled)
|
|
126
|
+
return;
|
|
94
127
|
try {
|
|
95
|
-
fs.mkdirSync(this.memoryDir, { recursive: true });
|
|
96
|
-
const normalized = {
|
|
128
|
+
fs.mkdirSync(this.memoryDir, { recursive: true, mode: 0o700 });
|
|
129
|
+
const normalized = this.brainStore.update((current) => ({
|
|
130
|
+
...current,
|
|
97
131
|
...brain,
|
|
98
|
-
|
|
132
|
+
version: 2,
|
|
133
|
+
privacyMode: 'local-only',
|
|
134
|
+
workspaceName: brain.workspaceName || current.workspaceName || path.basename(this.workspacePath),
|
|
99
135
|
updatedAt: nowIso(),
|
|
100
|
-
items: this.dedupeAndTrim(brain.items || []),
|
|
101
|
-
};
|
|
102
|
-
fs.writeFileSync(this.brainPath, JSON.stringify(normalized, null, 2) + '\n', 'utf8');
|
|
136
|
+
items: this.dedupeAndTrim([...(current.items || []), ...(brain.items || [])]),
|
|
137
|
+
})).data;
|
|
103
138
|
this.writeMarkdownViews(normalized);
|
|
104
139
|
}
|
|
105
140
|
catch {
|
|
@@ -107,6 +142,8 @@ export class ProjectMemoryService {
|
|
|
107
142
|
}
|
|
108
143
|
}
|
|
109
144
|
remember(type, text, options = {}) {
|
|
145
|
+
if (!this.enabled)
|
|
146
|
+
return;
|
|
110
147
|
const clean = clampText(text);
|
|
111
148
|
if (!clean || clean.length < 12)
|
|
112
149
|
return;
|
|
@@ -116,6 +153,8 @@ export class ProjectMemoryService {
|
|
|
116
153
|
this.saveBrain(brain);
|
|
117
154
|
}
|
|
118
155
|
rememberConversation(messages, options = {}) {
|
|
156
|
+
if (!this.enabled)
|
|
157
|
+
return;
|
|
119
158
|
const recent = (messages || []).slice(-16);
|
|
120
159
|
const candidates = [];
|
|
121
160
|
for (const message of recent) {
|
|
@@ -151,6 +190,8 @@ export class ProjectMemoryService {
|
|
|
151
190
|
this.saveBrain(brain);
|
|
152
191
|
}
|
|
153
192
|
buildContextForPrompt(prompt) {
|
|
193
|
+
if (!this.enabled)
|
|
194
|
+
return '';
|
|
154
195
|
const items = this.retrieveRelevantMemory(prompt, MAX_CONTEXT_ITEMS);
|
|
155
196
|
if (items.length === 0)
|
|
156
197
|
return '';
|
|
@@ -205,6 +246,8 @@ export class ProjectMemoryService {
|
|
|
205
246
|
itemCount: brain.items.length,
|
|
206
247
|
updatedAt: brain.updatedAt,
|
|
207
248
|
typeCounts,
|
|
249
|
+
enabled: this.enabled,
|
|
250
|
+
privacyMode: 'local-only',
|
|
208
251
|
};
|
|
209
252
|
}
|
|
210
253
|
createItem(type, text, options) {
|
|
@@ -270,10 +313,20 @@ export class ProjectMemoryService {
|
|
|
270
313
|
continue;
|
|
271
314
|
const heading = this.headingForType(type);
|
|
272
315
|
const lines = [`# ${heading}`, '', ...items.map((item) => `- ${item.text}`)];
|
|
273
|
-
|
|
316
|
+
this.atomicWrite(path.join(this.memoryDir, `${type}s.md`), lines.join('\n') + '\n');
|
|
274
317
|
allLines.push(`## ${heading}`, '', ...items.slice(-40).map((item) => `- ${item.text}`), '');
|
|
275
318
|
}
|
|
276
|
-
|
|
319
|
+
this.atomicWrite(path.join(this.memoryDir, 'README.md'), allLines.join('\n').trimEnd() + '\n');
|
|
320
|
+
}
|
|
321
|
+
atomicWrite(targetPath, content) {
|
|
322
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_BRAIN_BYTES) {
|
|
323
|
+
throw new Error('Project memory write exceeds size limit');
|
|
324
|
+
}
|
|
325
|
+
const temporary = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
326
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600 });
|
|
327
|
+
fs.renameSync(temporary, targetPath);
|
|
328
|
+
if (process.platform !== 'win32')
|
|
329
|
+
fs.chmodSync(targetPath, 0o600);
|
|
277
330
|
}
|
|
278
331
|
headingForType(type) {
|
|
279
332
|
switch (type) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PromptWorkspaceResolutionOptions {
|
|
2
|
+
baseWorkspace: string;
|
|
3
|
+
currentProjectPath: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
configuredProjectRoot?: string | null;
|
|
6
|
+
additionalRoots?: string[];
|
|
7
|
+
allowUnscoped?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface PromptWorkspaceResolution {
|
|
10
|
+
resolvedPath: string | null;
|
|
11
|
+
rejectedPath?: string;
|
|
12
|
+
allowedRoots: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function resolvePromptWorkspace(prompt: string, options: PromptWorkspaceResolutionOptions): PromptWorkspaceResolution;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
function extractExistingPath(prompt) {
|
|
4
|
+
const windowsMatch = prompt.match(/([A-Za-z]:[\\/][^\s<>"|?*]*)/);
|
|
5
|
+
const candidates = windowsMatch ? [windowsMatch[1]] : [];
|
|
6
|
+
if (!/\b(?:https?|ftp):\/\//i.test(prompt)) {
|
|
7
|
+
const unixMatch = prompt.match(/(\/(?:[^\s<>"|?*]|\s+(?![^\s<>"|?*]))*)/);
|
|
8
|
+
if (unixMatch) {
|
|
9
|
+
candidates.push(unixMatch[1]
|
|
10
|
+
.trim()
|
|
11
|
+
.replace(/\s+(and|or|at|in|the|to|for|with|from|by|on)$/i, '')
|
|
12
|
+
.replace(/[.,;!?:)\]]*$/, '')
|
|
13
|
+
.trim());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (!candidate || candidate === '/')
|
|
18
|
+
continue;
|
|
19
|
+
try {
|
|
20
|
+
const resolved = path.resolve(candidate);
|
|
21
|
+
if (fs.existsSync(resolved))
|
|
22
|
+
return fs.realpathSync(resolved);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Continue to the next syntactic candidate.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function canonicalDirectory(rawValue) {
|
|
31
|
+
const value = String(rawValue || '').trim();
|
|
32
|
+
if (!value)
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const resolved = path.resolve(value);
|
|
36
|
+
if (!fs.statSync(resolved).isDirectory())
|
|
37
|
+
return null;
|
|
38
|
+
return fs.realpathSync(resolved);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isWithin(candidate, root) {
|
|
45
|
+
const relative = path.relative(root, candidate);
|
|
46
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
47
|
+
}
|
|
48
|
+
export function resolvePromptWorkspace(prompt, options) {
|
|
49
|
+
const explicitPath = extractExistingPath(prompt);
|
|
50
|
+
const allowedRoots = [
|
|
51
|
+
options.baseWorkspace,
|
|
52
|
+
options.currentProjectPath,
|
|
53
|
+
options.cwd,
|
|
54
|
+
options.configuredProjectRoot,
|
|
55
|
+
...(options.additionalRoots || []),
|
|
56
|
+
].map(canonicalDirectory).filter((entry) => Boolean(entry));
|
|
57
|
+
const uniqueRoots = [...new Set(allowedRoots)];
|
|
58
|
+
if (!explicitPath)
|
|
59
|
+
return { resolvedPath: null, allowedRoots: uniqueRoots };
|
|
60
|
+
if (options.allowUnscoped || uniqueRoots.some((root) => isWithin(explicitPath, root))) {
|
|
61
|
+
return { resolvedPath: explicitPath, allowedRoots: uniqueRoots };
|
|
62
|
+
}
|
|
63
|
+
return { resolvedPath: null, rejectedPath: explicitPath, allowedRoots: uniqueRoots };
|
|
64
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ReleaseInstallResult = {
|
|
2
|
+
version: string;
|
|
3
|
+
versionDirectory: string;
|
|
4
|
+
currentLink: string;
|
|
5
|
+
previousTarget: string | null;
|
|
6
|
+
binDirectory: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function installReleaseTransaction(packageSpec: string, expectedVersion: string, options?: {
|
|
9
|
+
homeDirectory?: string;
|
|
10
|
+
npmRegistry?: string;
|
|
11
|
+
}): ReleaseInstallResult;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { getReleasePolicy } from './release-policy.js';
|
|
7
|
+
import { safeChildProcessEnv } from './secret-policy.js';
|
|
8
|
+
import { assertProcessNetworkAllowed } from './process-policy.js';
|
|
9
|
+
function npmExecutable() { return process.platform === 'win32' ? 'npm.cmd' : 'npm'; }
|
|
10
|
+
function readInstalledIdentity(prefix) {
|
|
11
|
+
const packageDirectory = path.join(prefix, 'node_modules', 'vigthoria-cli');
|
|
12
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'));
|
|
13
|
+
return { name: String(manifest.name || ''), version: String(manifest.version || ''), entry: path.join(packageDirectory, 'dist', 'index.js') };
|
|
14
|
+
}
|
|
15
|
+
function smokeCandidate(prefix, expectedVersion, homeDirectory) {
|
|
16
|
+
const identity = readInstalledIdentity(prefix);
|
|
17
|
+
if (identity.name !== getReleasePolicy().packageName || identity.version !== expectedVersion)
|
|
18
|
+
throw new Error('Staged package identity does not match the selected release.');
|
|
19
|
+
const environment = {
|
|
20
|
+
...safeChildProcessEnv(),
|
|
21
|
+
HOME: homeDirectory,
|
|
22
|
+
USERPROFILE: homeDirectory,
|
|
23
|
+
VIGTHORIA_OFFLINE: '1',
|
|
24
|
+
VIGTHORIA_NO_BANNER: '1',
|
|
25
|
+
VIGTHORIA_NO_UPDATE_CHECK: '1',
|
|
26
|
+
NO_COLOR: '1',
|
|
27
|
+
};
|
|
28
|
+
for (const args of [['--version'], ['--help'], ['doctor', '--json']]) {
|
|
29
|
+
const output = execFileSync(process.execPath, [identity.entry, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: 30_000, env: environment });
|
|
30
|
+
if (args[0] === '--version' && output.trim() !== expectedVersion)
|
|
31
|
+
throw new Error('Staged CLI banner does not match the selected release.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function writeLaunchers(binDirectory, currentLink) {
|
|
35
|
+
fs.mkdirSync(binDirectory, { recursive: true, mode: 0o700 });
|
|
36
|
+
const entry = path.join(currentLink, 'node_modules', 'vigthoria-cli', 'dist', 'index.js');
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
const body = `@echo off\r\n"${process.execPath}" "${entry}" %*\r\n`;
|
|
39
|
+
for (const name of ['vigthoria.cmd', 'vig.cmd', 'vigthoria-chat.cmd']) {
|
|
40
|
+
const temporary = path.join(binDirectory, `.${name}.${randomUUID()}.tmp`);
|
|
41
|
+
fs.writeFileSync(temporary, body, { flag: 'wx' });
|
|
42
|
+
fs.renameSync(temporary, path.join(binDirectory, name));
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const quotedEntry = entry.replaceAll("'", "'\\''");
|
|
47
|
+
const body = `#!/bin/sh\nexec '${process.execPath}' '${quotedEntry}' "$@"\n`;
|
|
48
|
+
for (const name of ['vigthoria', 'vig', 'vigthoria-chat']) {
|
|
49
|
+
const temporary = path.join(binDirectory, `.${name}.${randomUUID()}.tmp`);
|
|
50
|
+
fs.writeFileSync(temporary, body, { flag: 'wx', mode: 0o755 });
|
|
51
|
+
fs.renameSync(temporary, path.join(binDirectory, name));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function installReleaseTransaction(packageSpec, expectedVersion, options = {}) {
|
|
55
|
+
const policy = getReleasePolicy();
|
|
56
|
+
const homeDirectory = path.resolve(options.homeDirectory || os.homedir());
|
|
57
|
+
const cliRoot = path.join(homeDirectory, path.normalize(policy.installation.versionsDirectory), '..');
|
|
58
|
+
const versionsDirectory = path.join(homeDirectory, policy.installation.versionsDirectory);
|
|
59
|
+
const currentLink = path.join(homeDirectory, policy.installation.currentLink);
|
|
60
|
+
const binDirectory = path.join(homeDirectory, policy.installation.binDirectory);
|
|
61
|
+
fs.mkdirSync(versionsDirectory, { recursive: true, mode: 0o700 });
|
|
62
|
+
const stage = path.join(cliRoot, `.stage-${randomUUID()}`);
|
|
63
|
+
const destination = path.join(versionsDirectory, expectedVersion);
|
|
64
|
+
if (fs.existsSync(destination))
|
|
65
|
+
throw new Error(`Version ${expectedVersion} is already installed; refusing to overwrite it.`);
|
|
66
|
+
fs.mkdirSync(stage, { recursive: false, mode: 0o700 });
|
|
67
|
+
let switched = false;
|
|
68
|
+
let previousTarget = null;
|
|
69
|
+
try {
|
|
70
|
+
const npm = npmExecutable();
|
|
71
|
+
const args = ['install', '--prefix', stage, '--ignore-scripts', '--no-audit', '--no-fund', '--install-strategy=nested'];
|
|
72
|
+
const localArchive = path.isAbsolute(packageSpec) && fs.statSync(packageSpec, { throwIfNoEntry: false })?.isFile();
|
|
73
|
+
if (localArchive && process.env.VIGTHORIA_OFFLINE === '1')
|
|
74
|
+
args.push('--offline');
|
|
75
|
+
if (options.npmRegistry)
|
|
76
|
+
args.push(`--registry=${options.npmRegistry}`);
|
|
77
|
+
args.push(packageSpec);
|
|
78
|
+
if (!(localArchive && args.includes('--offline')))
|
|
79
|
+
assertProcessNetworkAllowed(npm, args);
|
|
80
|
+
execFileSync(npm, args, {
|
|
81
|
+
stdio: 'pipe',
|
|
82
|
+
windowsHide: true,
|
|
83
|
+
timeout: 180_000,
|
|
84
|
+
env: {
|
|
85
|
+
...safeChildProcessEnv(),
|
|
86
|
+
npm_config_cache: path.join(cliRoot, '.npm-cache'),
|
|
87
|
+
npm_config_ignore_scripts: 'true',
|
|
88
|
+
npm_config_audit: 'false',
|
|
89
|
+
npm_config_fund: 'false',
|
|
90
|
+
npm_config_update_notifier: 'false',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
smokeCandidate(stage, expectedVersion, homeDirectory);
|
|
94
|
+
fs.renameSync(stage, destination);
|
|
95
|
+
const currentStat = fs.lstatSync(currentLink, { throwIfNoEntry: false });
|
|
96
|
+
if (currentStat && !currentStat.isSymbolicLink())
|
|
97
|
+
throw new Error('Current release pointer is not a managed symbolic link; refusing to overwrite it.');
|
|
98
|
+
if (currentStat?.isSymbolicLink())
|
|
99
|
+
previousTarget = fs.readlinkSync(currentLink);
|
|
100
|
+
const temporaryLink = `${currentLink}.next-${randomUUID()}`;
|
|
101
|
+
fs.symlinkSync(destination, temporaryLink, process.platform === 'win32' ? 'junction' : 'dir');
|
|
102
|
+
try {
|
|
103
|
+
fs.renameSync(temporaryLink, currentLink);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (process.platform !== 'win32' || !fs.existsSync(currentLink))
|
|
107
|
+
throw error;
|
|
108
|
+
const backup = `${currentLink}.previous-${randomUUID()}`;
|
|
109
|
+
fs.renameSync(currentLink, backup);
|
|
110
|
+
try {
|
|
111
|
+
fs.renameSync(temporaryLink, currentLink);
|
|
112
|
+
}
|
|
113
|
+
catch (switchError) {
|
|
114
|
+
fs.renameSync(backup, currentLink);
|
|
115
|
+
throw switchError;
|
|
116
|
+
}
|
|
117
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
switched = true;
|
|
120
|
+
writeLaunchers(binDirectory, currentLink);
|
|
121
|
+
return { version: expectedVersion, versionDirectory: destination, currentLink, previousTarget, binDirectory };
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (switched) {
|
|
125
|
+
const rollbackLink = `${currentLink}.rollback-${randomUUID()}`;
|
|
126
|
+
try {
|
|
127
|
+
if (previousTarget) {
|
|
128
|
+
fs.symlinkSync(previousTarget, rollbackLink, process.platform === 'win32' ? 'junction' : 'dir');
|
|
129
|
+
fs.renameSync(rollbackLink, currentLink);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
fs.unlinkSync(currentLink);
|
|
133
|
+
}
|
|
134
|
+
switched = false;
|
|
135
|
+
}
|
|
136
|
+
catch (rollbackError) {
|
|
137
|
+
const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
138
|
+
throw new Error(`Release activation failed and rollback failed: ${detail}`, { cause: error });
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
try {
|
|
142
|
+
fs.unlinkSync(rollbackLink);
|
|
143
|
+
}
|
|
144
|
+
catch { }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!switched && fs.existsSync(destination))
|
|
148
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (fs.existsSync(stage))
|
|
153
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type ReleasePolicy = {
|
|
2
|
+
schemaVersion: number;
|
|
3
|
+
packageName: string;
|
|
4
|
+
manifest: {
|
|
5
|
+
schemaVersion: number;
|
|
6
|
+
maximumBytes: number;
|
|
7
|
+
requiredChannels: string[];
|
|
8
|
+
allowedChannels: string[];
|
|
9
|
+
};
|
|
10
|
+
origins: {
|
|
11
|
+
manifest: string;
|
|
12
|
+
artifacts: string;
|
|
13
|
+
allowedHosts: string[];
|
|
14
|
+
port: number;
|
|
15
|
+
maximumRedirects: number;
|
|
16
|
+
};
|
|
17
|
+
artifact: {
|
|
18
|
+
maximumBytes: number;
|
|
19
|
+
filenamePattern: string;
|
|
20
|
+
};
|
|
21
|
+
signature: {
|
|
22
|
+
algorithm: 'Ed25519';
|
|
23
|
+
trustedKeys: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
channels: Record<string, {
|
|
26
|
+
allowPrerelease: boolean;
|
|
27
|
+
allowDowngrade: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
installation: {
|
|
30
|
+
versionsDirectory: string;
|
|
31
|
+
currentLink: string;
|
|
32
|
+
binDirectory: string;
|
|
33
|
+
preservePrevious: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export type ReleaseEvidence = {
|
|
37
|
+
url: string;
|
|
38
|
+
sha256: string;
|
|
39
|
+
};
|
|
40
|
+
export type ReleaseManifestEntry = {
|
|
41
|
+
version: string;
|
|
42
|
+
url: string;
|
|
43
|
+
sha256: string;
|
|
44
|
+
size: number;
|
|
45
|
+
archiveFilename: string;
|
|
46
|
+
packageName: string;
|
|
47
|
+
packageVersion: string;
|
|
48
|
+
publishedAt: string;
|
|
49
|
+
signature: {
|
|
50
|
+
algorithm: 'Ed25519';
|
|
51
|
+
keyId: string;
|
|
52
|
+
url: string;
|
|
53
|
+
};
|
|
54
|
+
sbom: ReleaseEvidence;
|
|
55
|
+
provenance: ReleaseEvidence;
|
|
56
|
+
notes?: string;
|
|
57
|
+
};
|
|
58
|
+
export declare function getReleasePolicy(): ReleasePolicy;
|
|
59
|
+
export declare function assertReleaseUrl(raw: unknown, kind?: 'manifest' | 'artifact' | 'signature' | 'evidence'): URL;
|
|
60
|
+
export declare function parseReleaseManifest(input: unknown, channel?: string, serializedBytes?: number): ReleaseManifestEntry;
|
|
61
|
+
export declare function parseReleaseManifestText(text: string, channel?: string): ReleaseManifestEntry;
|
|
62
|
+
export declare function releaseSignaturePayload(channel: string, entry: ReleaseManifestEntry): Buffer;
|
|
63
|
+
export declare function verifyReleaseSignature(channel: string, entry: ReleaseManifestEntry, detachedBase64: string, trustedKeys?: Record<string, string>): void;
|
|
64
|
+
export declare function assertReleaseTransition(currentVersion: string, entry: ReleaseManifestEntry, channel: string, allowDowngrade?: boolean): void;
|