vibe-forge 0.8.3 → 0.8.6
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/.claude/commands/clear-attention.md +63 -63
- package/.claude/commands/compact-context.md +52 -52
- package/.claude/commands/need-help.md +77 -77
- package/.claude/commands/update-status.md +64 -64
- package/.claude/commands/worker-loop.md +106 -106
- package/.claude/scripts/setup-worker-loop.sh +45 -45
- package/LICENSE +21 -21
- package/README.md +211 -211
- package/bin/cli.js +303 -37
- package/bin/dashboard/api/agents.js +333 -333
- package/bin/dashboard/api/dispatch.js +507 -507
- package/bin/dashboard/api/tasks.js +416 -416
- package/bin/dashboard/public/assets/index-Dm2PgE2m.js +2 -0
- package/bin/dashboard/public/index.html +13 -13
- package/bin/dashboard/server.js +574 -645
- package/config/agent-manifest.yaml +237 -237
- package/config/agents.json +207 -207
- package/config/task-types.yaml +111 -111
- package/context/agent-overrides/README.md +41 -41
- package/context/architecture.md +42 -42
- package/context/modern-conventions.md +129 -129
- package/docs/agents.md +473 -473
- package/docs/architecture.md +194 -194
- package/docs/commands.md +451 -451
- package/docs/security.md +195 -195
- package/package.json +2 -3
- package/bin/dashboard/public/assets/index-BpHfsx1r.js +0 -2
package/bin/cli.js
CHANGED
|
@@ -9,17 +9,217 @@
|
|
|
9
9
|
* npx vibe-forge --help Show help
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const { execSync, spawn } = require('child_process');
|
|
12
|
+
const { execSync, spawn, spawnSync } = require('child_process');
|
|
13
13
|
const fs = require('fs');
|
|
14
|
+
const https = require('https');
|
|
14
15
|
const path = require('path');
|
|
15
16
|
const os = require('os');
|
|
16
17
|
|
|
17
18
|
// Read version from package.json (single source of truth)
|
|
18
19
|
const packageJson = require(path.join(__dirname, '..', 'package.json'));
|
|
19
20
|
const VERSION = packageJson.version;
|
|
21
|
+
const PKG_NAME = packageJson.name;
|
|
20
22
|
const REPO_URL = 'https://github.com/sugar-crash-studios/vibe-forge.git';
|
|
21
23
|
const FORGE_DIR = '_vibe-forge';
|
|
22
24
|
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Environment allowlist for child processes.
|
|
27
|
+
//
|
|
28
|
+
// forge-setup.sh runs with full user privileges, so we pass only the minimum
|
|
29
|
+
// env it needs instead of the entire parent environment. This keeps ambient
|
|
30
|
+
// secrets (AWS_*, GITHUB_TOKEN, NPM_TOKEN, OPENAI_API_KEY, ANTHROPIC_API_KEY,
|
|
31
|
+
// DATABASE_URL, STRIPE_*, etc.) from leaking into setup scripts if a user's
|
|
32
|
+
// shell happens to have them set at invocation time.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Frozen so third parties (or a poisoned import chain) cannot push a secret
|
|
35
|
+
// name onto the allowlist at runtime and leak it into the next buildChildEnv
|
|
36
|
+
// call. The whole point of the allowlist is defense-in-depth, so the exported
|
|
37
|
+
// surface is treated as immutable.
|
|
38
|
+
const ENV_ALLOWLIST = Object.freeze([
|
|
39
|
+
// POSIX basics
|
|
40
|
+
'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TERM', 'TMPDIR',
|
|
41
|
+
// Locale
|
|
42
|
+
'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES',
|
|
43
|
+
// Windows basics
|
|
44
|
+
'SYSTEMROOT', 'SYSTEMDRIVE', 'COMSPEC', 'USERPROFILE', 'LOCALAPPDATA',
|
|
45
|
+
'APPDATA', 'TEMP', 'TMP', 'USERNAME', 'PATHEXT', 'WINDIR',
|
|
46
|
+
'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'PROGRAMW6432',
|
|
47
|
+
'PROCESSOR_ARCHITECTURE', 'PROCESSOR_IDENTIFIER',
|
|
48
|
+
// Git Bash / MSYS
|
|
49
|
+
'MSYSTEM', 'MINGW_PREFIX', 'MINGW_CHOST',
|
|
50
|
+
// Proxies (needed for git/npm in corporate networks)
|
|
51
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY',
|
|
52
|
+
'http_proxy', 'https_proxy', 'no_proxy',
|
|
53
|
+
// TLS CA bundles — consistent partner of the proxy vars above. Corporate
|
|
54
|
+
// networks with intercepting proxies need these or node/git fail TLS.
|
|
55
|
+
'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
|
|
56
|
+
'GIT_SSL_CAINFO', 'GIT_SSL_CAPATH',
|
|
57
|
+
// Claude Code — setup validates `claude --version`, which needs this on Windows
|
|
58
|
+
'CLAUDE_CODE_GIT_BASH_PATH',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function buildChildEnv(ambient, extras = {}) {
|
|
62
|
+
// Reject non-object extras loudly. String/number/boolean extras silently
|
|
63
|
+
// spread wrong shapes (e.g. Object.assign({}, 'abc') → {0:'a',1:'b',2:'c'}),
|
|
64
|
+
// which produces a silently-wrong env instead of a caller-visible error.
|
|
65
|
+
if (extras !== undefined && extras !== null && (typeof extras !== 'object' || Array.isArray(extras))) {
|
|
66
|
+
throw new TypeError(`buildChildEnv: extras must be a plain object, got ${typeof extras}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const source = ambient || {};
|
|
70
|
+
const env = {};
|
|
71
|
+
for (const key of ENV_ALLOWLIST) {
|
|
72
|
+
if (source[key] !== undefined) {
|
|
73
|
+
env[key] = source[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Merge extras, but treat `undefined` as a no-op rather than overwriting the
|
|
78
|
+
// allowlisted value with an undefined. The source-side loop deliberately
|
|
79
|
+
// skips undefined keys; extras should follow the same rule for symmetry.
|
|
80
|
+
if (extras) {
|
|
81
|
+
for (const key of Object.keys(extras)) {
|
|
82
|
+
if (extras[key] !== undefined) {
|
|
83
|
+
env[key] = extras[key];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return env;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Tag-based supply-chain helpers
|
|
93
|
+
//
|
|
94
|
+
// forge init — clones the tag matching the CLI's own npm version, so the
|
|
95
|
+
// installed tarball and the git tree are always in sync.
|
|
96
|
+
// forge update — queries the npm registry directly via HTTPS (no subprocess)
|
|
97
|
+
// to find the latest published version, then fetches that tag.
|
|
98
|
+
// Using https.get instead of execFileSync('npm', ['view', ...])
|
|
99
|
+
// avoids ENOENT on Windows where npm is `npm.cmd` and Node's
|
|
100
|
+
// CVE-2024-27980 fix blocks .cmd/.bat spawn without shell:true.
|
|
101
|
+
// Direct HTTPS also removes the PATH-shadowing attack class.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
// Semver regex per semver.org spec:
|
|
105
|
+
// - §2/§4: numeric identifiers in major/minor/patch must not have leading zeros
|
|
106
|
+
// - §9: pre-release identifiers between dots must be non-empty (no "..")
|
|
107
|
+
// - §10: build metadata identifiers are alphanumeric+hyphen, non-empty between dots
|
|
108
|
+
// Note: leading zeros in NUMERIC pre-release identifiers (e.g. "1.0.0-01") are not
|
|
109
|
+
// rejected here — npm does not publish such versions in practice and tightening the
|
|
110
|
+
// pre-release numeric check would significantly complicate the pattern.
|
|
111
|
+
const SEMVER_RE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\w]+(?:\.[\w]+)*)?(?:\+[\w.-]+)?$/;
|
|
112
|
+
|
|
113
|
+
function buildTagRef(version) {
|
|
114
|
+
if (typeof version !== 'string' || !version) {
|
|
115
|
+
throw new TypeError(`buildTagRef: version must be a non-empty string, got ${JSON.stringify(version)}`);
|
|
116
|
+
}
|
|
117
|
+
// Strip any accidental leading 'v' so buildTagRef('v1.2.3') → 'v1.2.3'
|
|
118
|
+
// rather than 'vv1.2.3', which would not match the git tag.
|
|
119
|
+
const stripped = version.replace(/^v/, '');
|
|
120
|
+
// Validate semver format — rejects null bytes, newlines, spaces, and any
|
|
121
|
+
// other character that would produce a malformed or ambiguous git ref.
|
|
122
|
+
if (!SEMVER_RE.test(stripped)) {
|
|
123
|
+
throw new TypeError(`buildTagRef: invalid semver "${version}"`);
|
|
124
|
+
}
|
|
125
|
+
return `v${stripped}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildCloneArgs(repoUrl, tag, targetDir) {
|
|
129
|
+
if (typeof repoUrl !== 'string' || !repoUrl) throw new TypeError('buildCloneArgs: repoUrl must be a non-empty string');
|
|
130
|
+
if (typeof tag !== 'string' || !tag) throw new TypeError('buildCloneArgs: tag must be a non-empty string');
|
|
131
|
+
// Require the tag to be a v-prefixed semver string (as produced by buildTagRef).
|
|
132
|
+
// Defense-in-depth: prevents callers that skip buildTagRef from passing an
|
|
133
|
+
// adversarial string (e.g. '--upload-pack=evil') into the git argv array.
|
|
134
|
+
if (tag[0] !== 'v' || !SEMVER_RE.test(tag.slice(1))) {
|
|
135
|
+
throw new TypeError(`buildCloneArgs: tag must be a v-prefixed semver string, got "${tag}"`);
|
|
136
|
+
}
|
|
137
|
+
if (typeof targetDir !== 'string' || !targetDir) throw new TypeError('buildCloneArgs: targetDir must be a non-empty string');
|
|
138
|
+
return ['clone', '--depth', '1', '--branch', tag, repoUrl, targetDir];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Matches unscoped (e.g. "vibe-forge") and scoped (e.g. "@scope/pkg") npm
|
|
142
|
+
// package names per the npm package-name spec:
|
|
143
|
+
// - all lowercase; no uppercase
|
|
144
|
+
// - starts and ends with a letter or digit (not tilde, dot, or hyphen)
|
|
145
|
+
// - may contain hyphens, underscores, and dots in the middle
|
|
146
|
+
// - consecutive dots (..) are rejected via the negative lookahead
|
|
147
|
+
// - no path separators, tildes, or shell metacharacters
|
|
148
|
+
// Length limit (max 214 chars) is enforced separately in getLatestPublishedVersion.
|
|
149
|
+
const NPM_PKG_NAME_RE = /^(?!.*\.\.)(?:@[a-z0-9](?:[a-z0-9-._]*[a-z0-9])?\/)?[a-z0-9](?:[a-z0-9-._]*[a-z0-9])?$/;
|
|
150
|
+
|
|
151
|
+
function getLatestPublishedVersion(pkgName = PKG_NAME) {
|
|
152
|
+
if (
|
|
153
|
+
typeof pkgName !== 'string' ||
|
|
154
|
+
pkgName.length > 214 ||
|
|
155
|
+
!NPM_PKG_NAME_RE.test(pkgName)
|
|
156
|
+
) {
|
|
157
|
+
return Promise.reject(new TypeError(`getLatestPublishedVersion: invalid package name "${pkgName}"`));
|
|
158
|
+
}
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
// Captured in the outer closure so the timeout handler can also destroy the
|
|
161
|
+
// response stream, preventing buffered data/end events from racing the error.
|
|
162
|
+
let resRef = null;
|
|
163
|
+
const req = https.get(
|
|
164
|
+
`https://registry.npmjs.org/${pkgName}`,
|
|
165
|
+
{ headers: { Accept: 'application/vnd.npm.install-v1+json' } },
|
|
166
|
+
(res) => {
|
|
167
|
+
resRef = res;
|
|
168
|
+
// Forward response-stream errors (e.g. from res.destroy in size-cap path)
|
|
169
|
+
// to the promise rejection channel.
|
|
170
|
+
res.on('error', reject);
|
|
171
|
+
if (res.statusCode !== 200) {
|
|
172
|
+
// Drain the response body so the underlying socket is released back
|
|
173
|
+
// to the connection pool before we reject. Harmless for a CLI that
|
|
174
|
+
// exits immediately, but required for correctness in longer-lived use.
|
|
175
|
+
res.resume();
|
|
176
|
+
reject(new Error(`npm registry returned ${res.statusCode}`));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Cap the accumulated body at 1 MB. The abbreviated manifest format
|
|
180
|
+
// (application/vnd.npm.install-v1+json) is < 1 KB in practice.
|
|
181
|
+
const MAX_BODY = 1 * 1024 * 1024;
|
|
182
|
+
let body = '';
|
|
183
|
+
res.on('data', (chunk) => {
|
|
184
|
+
body += chunk;
|
|
185
|
+
if (body.length > MAX_BODY) {
|
|
186
|
+
res.destroy(new Error('npm registry response too large (> 1 MB)'));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
res.on('end', () => {
|
|
190
|
+
try {
|
|
191
|
+
const data = JSON.parse(body);
|
|
192
|
+
const version = data['dist-tags']?.latest;
|
|
193
|
+
if (!version) {
|
|
194
|
+
reject(new Error('No latest dist-tag in registry response'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Accept full semver including pre-release and build metadata.
|
|
198
|
+
// SEMVER_RE rejects leading zeros in major/minor/patch (semver §4)
|
|
199
|
+
// and empty pre-release identifiers (semver §9). See SEMVER_RE
|
|
200
|
+
// definition for caveats on pre-release numeric leading zeros.
|
|
201
|
+
if (!SEMVER_RE.test(version)) {
|
|
202
|
+
reject(new Error(`Registry returned unrecognized version: "${version}"`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
resolve(version);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
reject(err);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
req.on('error', reject);
|
|
213
|
+
req.setTimeout(10000, () => {
|
|
214
|
+
// Destroy both the request and the response stream (if already received)
|
|
215
|
+
// so that any buffered data/end events cannot race and resolve the promise
|
|
216
|
+
// after the timeout rejection.
|
|
217
|
+
req.destroy(new Error('npm registry request timed out after 10s'));
|
|
218
|
+
if (resRef) resRef.destroy();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
23
223
|
// Colors for terminal output
|
|
24
224
|
// NOTE: Intentionally duplicated from src/lib/colors.sh
|
|
25
225
|
// This is by design: cli.js runs standalone via npx before the rest of Vibe Forge
|
|
@@ -170,10 +370,7 @@ function runBashScript(scriptPath, args = []) {
|
|
|
170
370
|
const child = spawn(bashPath, [scriptPath, ...args], {
|
|
171
371
|
stdio: 'inherit',
|
|
172
372
|
cwd: process.cwd(),
|
|
173
|
-
env: {
|
|
174
|
-
...process.env,
|
|
175
|
-
CLAUDE_CODE_GIT_BASH_PATH: bashPath,
|
|
176
|
-
},
|
|
373
|
+
env: buildChildEnv(process.env, { CLAUDE_CODE_GIT_BASH_PATH: bashPath }),
|
|
177
374
|
});
|
|
178
375
|
|
|
179
376
|
child.on('close', (code) => {
|
|
@@ -209,18 +406,28 @@ async function initCommand() {
|
|
|
209
406
|
logSuccess('Prerequisites OK');
|
|
210
407
|
log('');
|
|
211
408
|
|
|
212
|
-
// Clone the repository
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
409
|
+
// Clone the repository at the tag matching this CLI version so the installed
|
|
410
|
+
// npm tarball and the git tree are always in sync. Uses spawnSync with an
|
|
411
|
+
// arg array (no shell-injection surface). Fails loudly if the tag doesn't
|
|
412
|
+
// exist rather than silently falling back to main.
|
|
413
|
+
const tag = buildTagRef(VERSION);
|
|
414
|
+
logInfo(`Cloning Vibe Forge ${tag} into ${FORGE_DIR}/...`);
|
|
415
|
+
const cloneResult = spawnSync(
|
|
416
|
+
'git',
|
|
417
|
+
buildCloneArgs(REPO_URL, tag, FORGE_DIR),
|
|
418
|
+
{
|
|
216
419
|
stdio: 'inherit',
|
|
217
420
|
cwd: process.cwd(),
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
421
|
+
env: buildChildEnv(process.env),
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
if (cloneResult.status !== 0) {
|
|
425
|
+
const spawnErr = cloneResult.error ? `: ${cloneResult.error.message}` : '';
|
|
426
|
+
logError(`Failed to clone ${REPO_URL} at tag ${tag}${spawnErr}`);
|
|
427
|
+
logInfo(`If the tag does not exist yet, publish ${PKG_NAME}@${VERSION} via the release workflow first.`);
|
|
222
428
|
process.exit(1);
|
|
223
429
|
}
|
|
430
|
+
logSuccess('Clone complete');
|
|
224
431
|
|
|
225
432
|
log('');
|
|
226
433
|
|
|
@@ -247,16 +454,26 @@ function validateAgentsConfig(forgeDir) {
|
|
|
247
454
|
const checkScript = path.join(forgeDir, 'bin', 'lib', 'check-aliases.js');
|
|
248
455
|
if (!fs.existsSync(checkScript)) return;
|
|
249
456
|
|
|
457
|
+
const agentsFile = path.join(forgeDir, 'config', 'agents.json');
|
|
458
|
+
const result = spawnSync('node', [checkScript, agentsFile], {
|
|
459
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
460
|
+
env: buildChildEnv(process.env),
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
if (result.status !== 0) {
|
|
464
|
+
logError('Agent alias collisions detected in config/agents.json');
|
|
465
|
+
const stderr = result.stderr ? result.stderr.toString().trim() : '';
|
|
466
|
+
logInfo(stderr || 'Run node src/lib/check-aliases.js for details');
|
|
467
|
+
logError('Fix collisions before using Vibe Forge.');
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
|
|
250
471
|
try {
|
|
251
|
-
const agentsFile = path.join(forgeDir, 'config', 'agents.json');
|
|
252
|
-
execSync(`node "${checkScript}" "${agentsFile}"`, { stdio: 'pipe' });
|
|
253
472
|
const config = JSON.parse(fs.readFileSync(agentsFile, 'utf8'));
|
|
254
473
|
const agentCount = Object.keys(config.agents || {}).length;
|
|
255
474
|
logSuccess(`Agent config valid (${agentCount} agents, no alias collisions)`);
|
|
256
|
-
} catch
|
|
257
|
-
logError('Agent
|
|
258
|
-
logInfo(err.stderr ? err.stderr.toString().trim() : 'Run node src/lib/check-aliases.js for details');
|
|
259
|
-
logError('Fix collisions before using Vibe Forge.');
|
|
475
|
+
} catch {
|
|
476
|
+
logError('Agent config validation passed, but agents.json could not be parsed.');
|
|
260
477
|
process.exit(1);
|
|
261
478
|
}
|
|
262
479
|
}
|
|
@@ -318,23 +535,61 @@ async function updateCommand() {
|
|
|
318
535
|
process.exit(1);
|
|
319
536
|
}
|
|
320
537
|
|
|
321
|
-
|
|
322
|
-
|
|
538
|
+
// Resolve the latest published version from the npm registry via direct
|
|
539
|
+
// HTTPS (no subprocess). Using execFileSync('npm', ['view', ...]) fails with
|
|
540
|
+
// ENOENT on Windows because npm is npm.cmd and Node's CVE-2024-27980 fix
|
|
541
|
+
// blocks .cmd/.bat spawn without shell:true. Direct HTTPS also removes the
|
|
542
|
+
// PATH-shadowing attack class entirely.
|
|
543
|
+
let latestVersion;
|
|
323
544
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
545
|
+
latestVersion = await getLatestPublishedVersion();
|
|
546
|
+
} catch (err) {
|
|
547
|
+
logError(`Could not query the latest ${PKG_NAME} version from npm: ${err.message}`);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const tag = buildTagRef(latestVersion);
|
|
552
|
+
logInfo(`Updating Vibe Forge to ${tag} (latest on npm)...`);
|
|
553
|
+
|
|
554
|
+
const childEnv = buildChildEnv(process.env);
|
|
555
|
+
|
|
556
|
+
// Fetch the specific tag only — no --force. If the local tag already exists
|
|
557
|
+
// pointing at a different SHA, git will reject the fetch and surface the
|
|
558
|
+
// conflict to the user. --force would silently accept tag relocations and
|
|
559
|
+
// apply them to every existing install on next update (Slag HIGH finding
|
|
560
|
+
// RT-20260411-001).
|
|
561
|
+
// Fetch only this specific tag refspec — no --tags. The --tags flag would
|
|
562
|
+
// additionally pull all remote tags via refs/tags/*:refs/tags/*, defeating
|
|
563
|
+
// the goal of fetching only the target version. The explicit refspec without
|
|
564
|
+
// a leading '+' already enforces the no-force constraint.
|
|
565
|
+
const fetchResult = spawnSync(
|
|
566
|
+
'git',
|
|
567
|
+
['fetch', 'origin', `refs/tags/${tag}:refs/tags/${tag}`],
|
|
568
|
+
{ stdio: 'inherit', cwd: targetDir, env: childEnv }
|
|
569
|
+
);
|
|
570
|
+
if (fetchResult.status !== 0) {
|
|
571
|
+
const spawnErr = fetchResult.error ? `: ${fetchResult.error.message}` : '';
|
|
572
|
+
logError(`Failed to fetch tag ${tag}${spawnErr}. If the local tag has drifted, delete it with 'git -C ${FORGE_DIR} tag -d ${tag}' and retry.`);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const resetResult = spawnSync(
|
|
577
|
+
'git',
|
|
578
|
+
['reset', '--hard', `refs/tags/${tag}`],
|
|
579
|
+
{ stdio: 'inherit', cwd: targetDir, env: childEnv }
|
|
580
|
+
);
|
|
581
|
+
if (resetResult.status !== 0) {
|
|
582
|
+
const spawnErr = resetResult.error ? `: ${resetResult.error.message}` : '';
|
|
583
|
+
logError(`Failed to check out tag ${tag}${spawnErr}.`);
|
|
336
584
|
process.exit(1);
|
|
337
585
|
}
|
|
586
|
+
|
|
587
|
+
// NOTE: _vibe-forge/node_modules is NOT updated here. If the new tag
|
|
588
|
+
// changed forge's own package.json dependencies, modules may be stale until
|
|
589
|
+
// the user re-runs forge-setup.sh or manually runs npm install inside
|
|
590
|
+
// _vibe-forge/. This is pre-existing behavior; a future improvement would
|
|
591
|
+
// re-invoke forge-setup.sh automatically after a successful tag switch.
|
|
592
|
+
logSuccess(`Update complete (now at ${tag})`);
|
|
338
593
|
}
|
|
339
594
|
|
|
340
595
|
// Main entry point
|
|
@@ -366,7 +621,18 @@ async function main() {
|
|
|
366
621
|
}
|
|
367
622
|
}
|
|
368
623
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
624
|
+
if (require.main === module) {
|
|
625
|
+
main().catch((err) => {
|
|
626
|
+
logError(err.message);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
module.exports = {
|
|
632
|
+
buildChildEnv,
|
|
633
|
+
ENV_ALLOWLIST,
|
|
634
|
+
buildTagRef,
|
|
635
|
+
buildCloneArgs,
|
|
636
|
+
getLatestPublishedVersion,
|
|
637
|
+
NPM_PKG_NAME_RE,
|
|
638
|
+
};
|