@vodmal/vdx-cli 0.13.0 → 0.13.1

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/README.md CHANGED
@@ -50,6 +50,9 @@ vdx doctor [--format=ansi|markdown|json]
50
50
  # Start your agent in a project per your profile (~/.vdx-environment.yaml or $VDX_ENVIRONMENT)
51
51
  vdx ai [project-path] [--new] [--restart] [--detach] [--dry-run] # default: cwd
52
52
 
53
+ # Print the installed version
54
+ vdx --version
55
+
53
56
  # MCP stdio server consumed by the Claude Code plugin
54
57
  vdx-mcp --project <path>
55
58
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vodmal/vdx-cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "vdx — unified lifecycle interface (up/down/build/test/check/fix) + versioned maturity audit with drift detection. Pairs with the vdx Claude Code plugin.",
5
5
  "keywords": [
6
6
  "vdx",
package/src/doctor.ts CHANGED
@@ -162,7 +162,7 @@ function checkNpmAuth(): CheckResult {
162
162
  id: 'npm-auth',
163
163
  label: 'npm auth',
164
164
  status: 'warning',
165
- message: 'not logged in — `vdx publish` will fail with E404',
165
+ message: 'not logged in — `vdx publish` refuses at pre-flight (npm-auth)',
166
166
  remedy: 'npm login',
167
167
  };
168
168
  }
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  reportDoctorJson,
20
20
  reportDoctorAnsi,
21
21
  } from './report.ts';
22
- import { runDoctor, looksLikeProject } from './doctor.ts';
22
+ import { runDoctor, looksLikeProject, readCliVersion } from './doctor.ts';
23
23
  import { planInit, writeInit, renderPlanSummary } from './init.ts';
24
24
  import {
25
25
  planPublish,
@@ -42,6 +42,7 @@ function usage(): never {
42
42
  vdx publish <patch|minor|major> [--dry-run] [--force]
43
43
  vdx doctor [--format=ansi|markdown|json] [--json]
44
44
  vdx ai [project_path] [--new] [--restart] [--detach] [--dry-run] (default: cwd)
45
+ vdx --version
45
46
  `,
46
47
  );
47
48
  process.exit(1);
@@ -283,7 +284,8 @@ function cmdAi(opts: ParsedArgs): void {
283
284
  }
284
285
 
285
286
  const parsed = parseArgs(process.argv);
286
- if (parsed.cmd === 'audit') cmdAudit(parsed);
287
+ if (parsed.cmd === '--version') process.stdout.write(readCliVersion() + '\n');
288
+ else if (parsed.cmd === 'audit') cmdAudit(parsed);
287
289
  else if (parsed.cmd === 'init') cmdInit(parsed);
288
290
  else if (parsed.cmd === 'publish') cmdPublish(parsed);
289
291
  else if (parsed.cmd === 'doctor') cmdDoctor(parsed);
package/src/publish.ts CHANGED
@@ -79,12 +79,21 @@ function isGitWorkingTreeClean(projectRoot: string): { ok: boolean; detail: stri
79
79
  }
80
80
  }
81
81
 
82
- function fetchPublishedVersion(packageName: string): string | null {
82
+ /** Runs npm with these args and returns stdout; throws with `stderr` on a non-zero exit. */
83
+ export type NpmRunner = (args: string[]) => string;
84
+
85
+ // A caller's `npm run -s` passes `npm_config_loglevel=silent` down, and npm then
86
+ // fails with an empty stderr — no E401 or E404 left to tell the cases apart.
87
+ const runNpm: NpmRunner = (args) =>
88
+ execFileSync('npm', args, {
89
+ encoding: 'utf8',
90
+ stdio: ['ignore', 'pipe', 'pipe'],
91
+ env: { ...process.env, npm_config_loglevel: 'error' },
92
+ });
93
+
94
+ function fetchPublishedVersion(packageName: string, npm: NpmRunner): string | null {
83
95
  try {
84
- const out = execFileSync('npm', ['view', packageName, 'version'], {
85
- encoding: 'utf8',
86
- stdio: ['ignore', 'pipe', 'pipe'],
87
- });
96
+ const out = npm(['view', packageName, 'version']);
88
97
  return out.trim() || null;
89
98
  } catch (e: any) {
90
99
  const stderr = String(e?.stderr ?? e?.message ?? '');
@@ -93,6 +102,54 @@ function fetchPublishedVersion(packageName: string): string | null {
93
102
  }
94
103
  }
95
104
 
105
+ /**
106
+ * The registry `npm publish` sends this package to, when it is not npm's
107
+ * default: `publishConfig.registry`, else the scope's registry from npm config.
108
+ */
109
+ export function publishRegistry(
110
+ pkg: { name?: string; publishConfig?: { registry?: string } },
111
+ npm: NpmRunner,
112
+ ): string | null {
113
+ if (pkg.publishConfig?.registry) return pkg.publishConfig.registry;
114
+ const scope = pkg.name?.match(/^(@[^/]+)\//)?.[1];
115
+ if (!scope) return null;
116
+ try {
117
+ const value = npm(['config', 'get', `${scope}:registry`]).trim();
118
+ return value && value !== 'undefined' ? value : null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Logged in where the package goes? Without it `npm publish` fails only after
126
+ * the bump and the pack, and with E404 on a scoped package, not E401 — an
127
+ * expired npm session reads as a missing package.
128
+ */
129
+ export function checkNpmAuth(registry: string | null, npm: NpmRunner): PreflightCheck {
130
+ const where = registry ?? 'npm';
131
+ const login = registry ? `npm login --registry ${registry}` : 'npm login';
132
+ try {
133
+ const who = npm(registry ? ['whoami', '--registry', registry] : ['whoami']).trim();
134
+ if (who) return { name: 'npm-auth', ok: true, message: `OK (logged in to ${where} as ${who})` };
135
+ return { name: 'npm-auth', ok: false, message: `npm whoami answered nothing for ${where}. Run: ${login}` };
136
+ } catch (e: any) {
137
+ const stderr = String(e?.stderr ?? e?.message ?? '');
138
+ if (/E401|ENEEDAUTH/.test(stderr)) {
139
+ return {
140
+ name: 'npm-auth',
141
+ ok: false,
142
+ message: `not logged in to ${where} (the npm session may have expired). Run: ${login}`,
143
+ };
144
+ }
145
+ return {
146
+ name: 'npm-auth',
147
+ ok: false,
148
+ message: `npm whoami failed for ${where}: ${stderr.split('\n').find((l) => l.trim()) ?? stderr}`,
149
+ };
150
+ }
151
+ }
152
+
96
153
  function checkMiseOverride(projectRoot: string): boolean {
97
154
  const miseToml = path.join(projectRoot, 'mise.toml');
98
155
  if (!fs.existsSync(miseToml)) return false;
@@ -116,6 +173,7 @@ export function planPublish(
116
173
  opts: PublishOptions,
117
174
  audit: AuditResult,
118
175
  ctx: Ctx,
176
+ npm: NpmRunner = runNpm,
119
177
  ): PublishPlan {
120
178
  if (checkMiseOverride(opts.projectRoot)) {
121
179
  return {
@@ -197,10 +255,12 @@ export function planPublish(
197
255
  : `release-artifact at ${raLevel}, need >= L3. Add files[], main/exports, license in package.json.`,
198
256
  });
199
257
 
258
+ preflight.push(checkNpmAuth(publishRegistry(pkg, npm), npm));
259
+
200
260
  let published: string | null = null;
201
261
  let registryError: string | null = null;
202
262
  try {
203
- published = fetchPublishedVersion(packageName);
263
+ published = fetchPublishedVersion(packageName, npm);
204
264
  } catch (e: any) {
205
265
  registryError = e?.message ?? String(e);
206
266
  }