@celilo/cli 0.9.0 → 0.10.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.
@@ -44,7 +44,7 @@ Each entry: `module id` — what it is — **provides** / **requires** capabilit
44
44
  - **forgejo** — self-hosted Forgejo git forge (git-over-SSH, OIDC, public ingress). **provides:** `source_forge`. **requires:** `public_web`, `idp`, `firewall`, `dns_registrar`. See `v2/CI_PIPELINE.md`.
45
45
  - **forgejo-runner** — host-mode (LXC) Forgejo Actions runner for unit/lint jobs; outbound-only, lives in dmz. **requires:** `source_forge`. See `v2/FORGEJO_RUNNER.md`.
46
46
  - **forgejo-builder** — VM-based, Docker-capable Forgejo Actions runner (`requires.system.type: vm`) for hermetic release builds + the cele2e suite. **requires:** `source_forge`.
47
- - **npm-cache-node** — self-hosted npm registry as a pass-through disk cache (proxies upstream). **requires:** `public_web`, `dns_registrar`. See `v2/NPM_CACHE_NODE.md`.
47
+ - **npm-cache-node** — self-hosted npm registry: a pass-through disk cache (proxies upstream) plus a `PUT /<pkg>` publish endpoint that stores locally published `@celilo/*` tarballs as authoritative local-origin (never overwritten by upstream). PUT is gated on a static operator-set publish token (`publish_tokens` secret, SHA-256 model; fail-closed — absent/bad token → 401); reads stay open (it's a mirror). **requires:** `public_web`, `dns_registrar`. See `v2/NPM_CACHE_NODE.md`.
48
48
 
49
49
  ## Applications
50
50
 
@@ -81,3 +81,4 @@ see `design/README.md`. Companion doc: [CELILO_CORE_MODULES.md](./CELILO_CORE_MO
81
81
 
82
82
  - **cele2e harness** — `packages/e2e/src/` — `runner.ts`, `container-manager.ts` (`startNetwork`, `reconnectNetwork`), `network-builder.ts` (`NetworkBuilder`).
83
83
  - **Run wrapper** — `infra/scripts/cele2e-run.sh` lives in the separate `infra/` clone, **not** in this repo. See the cele2e section of the repo-root `CLAUDE.md` for the operator workflow.
84
+ - **MCP server (agent-driven e2e)** — `packages/mcp-server/src/index.ts` — stdio MCP server exposing `start_run`/`run_status`/`run_result`/`stop_run` (detached cele2e runs read off the event bus, no ANSI scraping) + `env_check` (docker VM, run-lock, shared-infra, mgmt image CLI version, netapps). Dev/ops tool, `private`, not shipped to consumers.
@@ -101,4 +101,4 @@
101
101
  "breakpoints": true
102
102
  }
103
103
  ]
104
- }
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,7 +57,7 @@
57
57
  "@aws-sdk/client-s3": "^3.1024.0",
58
58
  "@celilo/capabilities": "^0.6.0",
59
59
  "@celilo/cli-display": "^0.1.9",
60
- "@celilo/event-bus": "^0.1.7",
60
+ "@celilo/event-bus": "^0.1.8",
61
61
  "@clack/prompts": "^1.1.0",
62
62
  "ajv": "^8.18.0",
63
63
  "drizzle-orm": "^0.36.4",
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
  import {
3
3
  WELL_KNOWN_CAPABILITIES,
4
- getCapabilityDataSchema,
5
4
  getSupportedCapabilities,
6
5
  getWellKnownCapability,
7
6
  isWellKnown,
@@ -166,64 +165,6 @@ describe('Well-Known Capabilities Registry', () => {
166
165
  });
167
166
  });
168
167
 
169
- describe('getCapabilityDataSchema', () => {
170
- test('should return data schema for public_web', () => {
171
- const schema = getCapabilityDataSchema('public_web');
172
-
173
- expect(schema).toEqual({
174
- server: {
175
- ip: {
176
- primary: '$self:target_ip',
177
- },
178
- port: 443,
179
- },
180
- });
181
- });
182
-
183
- test('should return data schema for dns_registrar', () => {
184
- const schema = getCapabilityDataSchema('dns_registrar');
185
-
186
- expect(schema).toEqual({
187
- provider: 'namecheap',
188
- domains: '$self:domains',
189
- supports: ['dynamic_dns_a_record'],
190
- });
191
- });
192
-
193
- test('should return data schema for auth', () => {
194
- const schema = getCapabilityDataSchema('auth');
195
-
196
- // Note: oidc.issuer_url was removed in MANIFEST_V2 D9 — auth providers
197
- // (e.g. authentik) now derive their issuer URL in their own manifest
198
- // and expose it through provides.capabilities[].data.
199
- expect(schema).toEqual({
200
- server: {
201
- ip: {
202
- primary: '$self:target_ip',
203
- },
204
- port: 9000,
205
- },
206
- });
207
- });
208
-
209
- test('should return cloned schema (not reference)', () => {
210
- const schema1 = getCapabilityDataSchema('public_web');
211
- const schema2 = getCapabilityDataSchema('public_web');
212
-
213
- // Modify one schema to test cloning
214
- // biome-ignore lint/suspicious/noExplicitAny: intentionally mutating to verify independent copies
215
- (schema1 as any).server.port = 8080;
216
-
217
- // Other schema should be unchanged
218
- // biome-ignore lint/suspicious/noExplicitAny: accessing mutated property for comparison
219
- expect((schema2 as any).server.port).toBe(443);
220
- });
221
-
222
- test('should throw error for unknown capability', () => {
223
- expect(() => getCapabilityDataSchema('unknown')).toThrow('Unknown capability: unknown');
224
- });
225
- });
226
-
227
168
  describe('Security zone requirements', () => {
228
169
  test('home lab public-facing services must be in DMZ', () => {
229
170
  expect(WELL_KNOWN_CAPABILITIES.public_web.required_zone).toBe('dmz');
@@ -227,12 +227,3 @@ export function validateZoneRequirement(
227
227
 
228
228
  return { valid: true };
229
229
  }
230
-
231
- /**
232
- * Get capability data schema with variables resolved
233
- * Note: This returns the template schema - actual variable resolution happens during generation
234
- */
235
- export function getCapabilityDataSchema(capabilityName: string): Record<string, unknown> {
236
- const capability = getWellKnownCapability(capabilityName);
237
- return structuredClone(capability.data_schema);
238
- }
@@ -125,6 +125,24 @@ export function readExternalProjectPaths(): string[] {
125
125
  return [];
126
126
  }
127
127
 
128
+ /**
129
+ * Read `NPM_PUBLISH_TARGET` from .env — the registry URL `celilo publish`
130
+ * points `bun publish --registry` at for @celilo/* packages (a deployed
131
+ * npm-cache-node). Unset → publish to npmjs (current behavior). See
132
+ * v2/NPM_CACHE_NODE.md Phase 3.1 / v2/PUBLILO_CLI.md decision 10.
133
+ */
134
+ export function readNpmPublishTarget(): string | null {
135
+ if (!existsSync(ENV_FILE)) return null;
136
+ const content = readFileSync(ENV_FILE, 'utf-8');
137
+ for (const line of content.split('\n')) {
138
+ const m = line.match(/^\s*NPM_PUBLISH_TARGET\s*=\s*(.+?)\s*$/);
139
+ if (!m) continue;
140
+ const raw = m[1].replace(/^["']|["']$/g, '').trim();
141
+ return raw || null;
142
+ }
143
+ return null;
144
+ }
145
+
128
146
  /**
129
147
  * Recursively find every package.json under a root, skipping
130
148
  * node_modules and common build-output dirs (so we don't try to rewrite
@@ -139,15 +139,13 @@ export interface RewriteOptions {
139
139
  }
140
140
 
141
141
  /**
142
- * Per-package pre-publish work. Currently only `@celilo/e2e` triggers
143
- * any of these (it ships netapps + registry server + caches inside its
144
- * tarball), but the plan still enumerates them so dry-run can show
145
- * what extra work the publish entails.
142
+ * Per-package pre-publish work. Currently only `@celilo/e2e` triggers this
143
+ * (it bundles the npm-compat registry server source inside its tarball).
144
+ * Sim-content caches (website/npm) and standard-module netapps are NOT
145
+ * bundled at publish — they're fetched from the public celilo sources at
146
+ * `cele2e build-infra` time instead (ce-qwz Decisions 2B + 3, ce-i2i).
146
147
  */
147
- export type PrePublishHookKind =
148
- | 'registryServerBundle'
149
- | 'rebuildE2eNetapps'
150
- | 'stageE2ePublishCaches';
148
+ export type PrePublishHookKind = 'registryServerBundle';
151
149
 
152
150
  /**
153
151
  * Single workspace package planned to publish (or explicitly skip) in
@@ -57,6 +57,7 @@ mock.module('./helpers', () => ({
57
57
  },
58
58
  listModuleDirs: () => [],
59
59
  readExternalProjectPaths: () => [],
60
+ readNpmPublishTarget: () => null,
60
61
  findPackageJsons: () => [],
61
62
  bareVersion: (s: string) => s.replace(/^[\s^~=><]+/, '').trim(),
62
63
  withOperator: (oldSpec: string, newVersion: string) => {
@@ -74,6 +75,10 @@ mock.module('./alpha', () => ({
74
75
  return { name: spec.slice(0, i), version: spec.slice(i + 1) };
75
76
  },
76
77
  stripAlphaSuffix: (v: string) => v.replace(/-alpha\.\d+$/, ''),
78
+ prereleaseDistTag: (v: string) => {
79
+ const dash = v.indexOf('-');
80
+ return dash === -1 ? undefined : v.slice(dash + 1).split('.')[0] || undefined;
81
+ },
77
82
  isAlphaVersion: (v: string) => /-alpha\.\d+$/.test(v),
78
83
  nextAlphaNumber: (name: string, semverCore: string) =>
79
84
  nextAlphaResponses[`${name}@${semverCore}`] ?? 0,
@@ -84,7 +89,7 @@ mock.module('./alpha', () => ({
84
89
  decideAlphaSkip: () => ({ skip: false }),
85
90
  }));
86
91
 
87
- const { planWorkspace } = await import('./workspace');
92
+ const { planWorkspace, buildPublishArgs } = await import('./workspace');
88
93
 
89
94
  function buildBaseMap(): Map<string, string> {
90
95
  const m = new Map<string, string>();
@@ -131,7 +136,7 @@ describe('planWorkspace', () => {
131
136
  expect(cli?.skipReason).toBeUndefined();
132
137
  });
133
138
 
134
- test('e2e gets the registry-bundle + netapps + cache hooks', () => {
139
+ test('e2e gets only the registry-bundle hook (caches/netapps fetched at build-infra, not bundled)', () => {
135
140
  publishedSet = new Set();
136
141
  const result = planWorkspace({
137
142
  mode: { kind: 'normal' },
@@ -141,11 +146,7 @@ describe('planWorkspace', () => {
141
146
  });
142
147
 
143
148
  const e2e = result.items.find((i) => i.pkg === 'packages/e2e');
144
- expect(e2e?.hooks).toEqual([
145
- 'registryServerBundle',
146
- 'rebuildE2eNetapps',
147
- 'stageE2ePublishCaches',
148
- ]);
149
+ expect(e2e?.hooks).toEqual(['registryServerBundle']);
149
150
  const cli = result.items.find((i) => i.pkg === 'apps/celilo');
150
151
  expect(cli?.hooks).toEqual([]);
151
152
  });
@@ -302,6 +303,42 @@ describe('planWorkspace', () => {
302
303
  });
303
304
  });
304
305
 
306
+ describe('buildPublishArgs registry target', () => {
307
+ test('no target → default registry (unchanged behavior)', () => {
308
+ expect(buildPublishArgs({ name: '@celilo/cli', tag: undefined }, null)).toEqual([
309
+ 'publish',
310
+ '--access',
311
+ 'public',
312
+ ]);
313
+ });
314
+
315
+ test('target set → --registry appended for @celilo/* package', () => {
316
+ expect(
317
+ buildPublishArgs({ name: '@celilo/cli', tag: undefined }, 'https://npm.example.test/'),
318
+ ).toEqual(['publish', '--access', 'public', '--registry', 'https://npm.example.test/']);
319
+ });
320
+
321
+ test('tag and registry both present', () => {
322
+ expect(
323
+ buildPublishArgs({ name: '@celilo/cli', tag: 'alpha' }, 'https://npm.example.test/'),
324
+ ).toEqual([
325
+ 'publish',
326
+ '--access',
327
+ 'public',
328
+ '--tag',
329
+ 'alpha',
330
+ '--registry',
331
+ 'https://npm.example.test/',
332
+ ]);
333
+ });
334
+
335
+ test('non-@celilo package never gets the private registry', () => {
336
+ expect(
337
+ buildPublishArgs({ name: 'some-other-pkg', tag: undefined }, 'https://npm.example.test/'),
338
+ ).toEqual(['publish', '--access', 'public']);
339
+ });
340
+ });
341
+
305
342
  describe('dependency order', () => {
306
343
  test('preserves PACKAGES order in planned items', () => {
307
344
  publishedSet = new Set();
@@ -17,18 +17,8 @@
17
17
  */
18
18
 
19
19
  import { spawnSync } from 'node:child_process';
20
- import {
21
- cpSync,
22
- existsSync,
23
- mkdirSync,
24
- readFileSync,
25
- readdirSync,
26
- rmSync,
27
- statSync,
28
- unlinkSync,
29
- writeFileSync,
30
- } from 'node:fs';
31
- import { join, relative } from 'node:path';
20
+ import { readFileSync, writeFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
32
22
  import {
33
23
  ALPHA_TAG,
34
24
  alphaSkipDecision,
@@ -36,7 +26,7 @@ import {
36
26
  prereleaseDistTag,
37
27
  stripAlphaSuffix,
38
28
  } from './alpha';
39
- import { REPO_ROOT, isPublished, readPkg } from './helpers';
29
+ import { REPO_ROOT, isPublished, readNpmPublishTarget, readPkg } from './helpers';
40
30
  import type {
41
31
  PackageJson,
42
32
  PublishMode,
@@ -74,13 +64,15 @@ export interface PlanWorkspaceOutput {
74
64
 
75
65
  /**
76
66
  * Pre-publish hooks that fire for @celilo/e2e (the only package that
77
- * needs them today). Listed in the WorkspaceItem so dry-run can show
78
- * "@celilo/e2e (will rebuild netapps, stage caches)" without executing
79
- * anything.
67
+ * needs one today): refresh the bundled npm-compat registry-server source.
68
+ * Sim-content caches and standard-module netapps are no longer staged into
69
+ * the tarball at publish — a monorepo-free consumer fetches them from the
70
+ * public celilo sources at `cele2e build-infra` time (ce-qwz Decisions
71
+ * 2B + 3, ce-i2i).
80
72
  */
81
73
  function workspaceHooksFor(pkg: string): WorkspaceItem['hooks'] {
82
74
  if (pkg !== 'packages/e2e') return [];
83
- return ['registryServerBundle', 'rebuildE2eNetapps', 'stageE2ePublishCaches'];
75
+ return ['registryServerBundle'];
84
76
  }
85
77
 
86
78
  /**
@@ -328,147 +320,23 @@ export async function verifyPublishedDeps(
328
320
  }
329
321
  }
330
322
 
331
- // ─── Per-package pre-publish hooks ─────────────────────────────────
332
-
333
- /**
334
- * Modules excluded from the @celilo/e2e netapps shipment:
335
- * - celilo-registry: bundles the bun-compiled registry server
336
- * binaries (~76 MB packed). Not used by typical consumer e2e
337
- * tests; including it would bloat the npm tarball past the SSL
338
- * transport's reliable window.
339
- * - archive: not a real module dir.
340
- */
341
- const E2E_NETAPP_EXCLUDES = new Set(['archive', 'celilo-registry']);
342
-
343
- /**
344
- * Pre-publish step for @celilo/e2e: package each (non-excluded)
345
- * module under `<root>/modules/` into `packages/e2e/netapps/`. Replaces
346
- * whatever was last left there by a local `cele2e build-infra` so the
347
- * shipped tarball is always built from the current branch, not the
348
- * publisher's last local development build.
349
- */
350
- export function rebuildE2eNetapps(repoRoot: string): void {
351
- const modulesDir = join(repoRoot, 'modules');
352
- const netappsDir = join(repoRoot, 'packages/e2e/netapps');
353
- const celiloWrapper = join(repoRoot, 'celilo');
354
-
355
- if (!existsSync(modulesDir)) {
356
- console.warn(
357
- `⚠ ${modulesDir} not found — skipping netapp rebuild. The published tarball will reuse whatever's currently in packages/e2e/netapps/.`,
358
- );
359
- return;
360
- }
361
- if (!existsSync(celiloWrapper)) {
362
- console.warn(
363
- `⚠ ${celiloWrapper} not found — skipping netapp rebuild. Same staleness risk as above.`,
364
- );
365
- return;
366
- }
367
-
368
- console.log('Rebuilding packages/e2e/netapps/ from infra/modules/...');
369
-
370
- for (const existing of readdirSync(netappsDir).filter((f) => f.endsWith('.netapp'))) {
371
- try {
372
- unlinkSync(join(netappsDir, existing));
373
- } catch {
374
- // Best effort
375
- }
376
- }
377
-
378
- const moduleDirs = readdirSync(modulesDir).filter((name) => {
379
- if (E2E_NETAPP_EXCLUDES.has(name)) return false;
380
- const dir = join(modulesDir, name);
381
- try {
382
- return statSync(dir).isDirectory() && existsSync(join(dir, 'manifest.yml'));
383
- } catch {
384
- return false;
385
- }
386
- });
387
-
388
- let okCount = 0;
389
- const failures: Array<{ name: string; stderr: string }> = [];
390
- for (const name of moduleDirs) {
391
- const moduleDir = join(modulesDir, name);
392
- const out = join(netappsDir, `${name}.netapp`);
393
- const r = spawnSync(celiloWrapper, ['package', moduleDir, '--output', out], {
394
- cwd: repoRoot,
395
- stdio: ['ignore', 'pipe', 'pipe'],
396
- encoding: 'utf-8',
397
- });
398
- if (r.status === 0) {
399
- okCount++;
400
- } else {
401
- failures.push({ name, stderr: (r.stderr ?? '').trim() || `exit ${r.status}` });
402
- }
403
- }
404
-
405
- if (failures.length > 0) {
406
- console.error(`✗ Failed to package ${failures.length} module(s):`);
407
- for (const f of failures) {
408
- console.error(` ${f.name}: ${f.stderr}`);
409
- }
410
- console.error(
411
- 'Aborting publish — shipping with missing netapps would silently break consumer e2e tests.',
412
- );
413
- process.exit(1);
414
- }
415
-
416
- console.log(
417
- `Refreshed ${okCount} netapp(s) in packages/e2e/netapps/ (excluded: ${[...E2E_NETAPP_EXCLUDES].join(', ')}).\n`,
418
- );
419
- }
420
-
421
323
  /**
422
- * Pre-publish step for @celilo/e2e: stage `.celilo-website-cache/` and
423
- * `.npm-registry-cache/` inside packages/e2e/ so the tarball ships with
424
- * everything Dockerfile.celilo-website-sim and Dockerfile.npm-registry-sim
425
- * need to COPY at docker-build time.
324
+ * Build the `bun publish` argv for a workspace item. When a private
325
+ * registry target is configured (a deployed npm-cache-node) AND the
326
+ * package is @celilo/*-scoped, point bun at it via --registry; otherwise
327
+ * publish to the default registry (npmjs — current behavior).
328
+ * v2/NPM_CACHE_NODE.md Phase 3.1 / v2/PUBLILO_CLI.md decision 10.
426
329
  */
427
- export function stageE2ePublishCaches(repoRoot: string): void {
428
- const websiteSrc = join(repoRoot, 'modules', 'celilo-website', 'site');
429
- const e2eDir = join(repoRoot, 'packages', 'e2e');
430
- const websiteCache = join(e2eDir, '.celilo-website-cache');
431
- const npmCache = join(e2eDir, '.npm-registry-cache');
432
- const packScript = join(e2eDir, 'scripts', 'pack-celilo-packages.ts');
433
-
434
- if (!existsSync(websiteSrc)) {
435
- console.error(`✗ ${websiteSrc} not found — cannot stage .celilo-website-cache.`);
436
- process.exit(1);
330
+ export function buildPublishArgs(
331
+ item: Pick<WorkspaceItem, 'name' | 'tag'>,
332
+ registryTarget: string | null,
333
+ ): string[] {
334
+ const args = ['publish', '--access', 'public'];
335
+ if (item.tag) args.push('--tag', item.tag);
336
+ if (registryTarget && item.name.startsWith('@celilo/')) {
337
+ args.push('--registry', registryTarget);
437
338
  }
438
- if (!existsSync(packScript)) {
439
- console.error(`✗ ${packScript} not found — cannot stage .npm-registry-cache.`);
440
- process.exit(1);
441
- }
442
-
443
- console.log('Staging .celilo-website-cache/ from modules/celilo-website/site/...');
444
- const installResult = spawnSync('bun', ['install'], { cwd: websiteSrc, stdio: 'pipe' });
445
- if (installResult.status !== 0) {
446
- console.error('✗ bun install for celilo-website failed:');
447
- console.error(installResult.stderr?.toString());
448
- process.exit(1);
449
- }
450
- const buildResult = spawnSync('bun', ['run', 'build'], { cwd: websiteSrc, stdio: 'pipe' });
451
- if (buildResult.status !== 0) {
452
- console.error('✗ bun run build for celilo-website failed:');
453
- console.error(buildResult.stderr?.toString());
454
- process.exit(1);
455
- }
456
- rmSync(websiteCache, { recursive: true, force: true });
457
- mkdirSync(websiteCache, { recursive: true });
458
- cpSync(join(websiteSrc, 'dist'), websiteCache, { recursive: true });
459
- console.log(`✓ Staged .celilo-website-cache/ (from ${relative(repoRoot, websiteSrc)}/dist/)\n`);
460
-
461
- console.log('Staging .npm-registry-cache/ from @celilo/* workspace tarballs...');
462
- const packResult = spawnSync('bun', ['run', packScript], { cwd: repoRoot, stdio: 'pipe' });
463
- if (packResult.status !== 0) {
464
- console.error('✗ pack-celilo-packages.ts failed:');
465
- console.error(packResult.stderr?.toString());
466
- process.exit(1);
467
- }
468
- const packed = existsSync(npmCache)
469
- ? readdirSync(npmCache).filter((f) => f.endsWith('.tgz'))
470
- : [];
471
- console.log(`✓ Staged .npm-registry-cache/ with ${packed.length} workspace tarball(s)\n`);
339
+ return args;
472
340
  }
473
341
 
474
342
  // ─── Executor ──────────────────────────────────────────────────────
@@ -486,6 +354,13 @@ export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<Pu
486
354
  const published: PublishResult['published'] = [];
487
355
  const skipped: string[] = [];
488
356
 
357
+ // When an npm-cache-node is configured, @celilo/* tarballs go there
358
+ // (the cache forwards upstream under policy); unset → npmjs.
359
+ const registryTarget = readNpmPublishTarget();
360
+ if (registryTarget) {
361
+ console.log(`\nPublishing @celilo/* to configured registry: ${registryTarget}`);
362
+ }
363
+
489
364
  for (const item of items) {
490
365
  const { pkg, name, baseVersion, versionToPublish } = item;
491
366
 
@@ -511,12 +386,6 @@ export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<Pu
511
386
  'Refreshed packages/e2e/registry-server/ bundle from packages/registry-server.\n',
512
387
  );
513
388
  }
514
- if (item.hooks.includes('rebuildE2eNetapps')) {
515
- rebuildE2eNetapps(REPO_ROOT);
516
- }
517
- if (item.hooks.includes('stageE2ePublishCaches')) {
518
- stageE2ePublishCaches(REPO_ROOT);
519
- }
520
389
 
521
390
  const { original: pkgJsonOriginal, rewrites: workspaceRewrites } = rewriteWorkspaceDeps(
522
391
  pkg,
@@ -548,8 +417,7 @@ export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<Pu
548
417
  process.exit(1);
549
418
  }
550
419
 
551
- const publishArgs = ['publish', '--access', 'public'];
552
- if (item.tag) publishArgs.push('--tag', item.tag);
420
+ const publishArgs = buildPublishArgs(item, registryTarget);
553
421
 
554
422
  let publishStatus: number | null = null;
555
423
  let publishError: unknown = null;
@@ -591,7 +459,15 @@ export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<Pu
591
459
  }
592
460
 
593
461
  if (workspaceRewrites.length > 0) {
594
- await verifyPublishedDeps(name, versionToPublish, workspaceRewrites);
462
+ // Verify against the same registry we published to — a cache node
463
+ // forwards upstream under policy, so the version may not be on
464
+ // npmjs yet.
465
+ await verifyPublishedDeps(
466
+ name,
467
+ versionToPublish,
468
+ workspaceRewrites,
469
+ registryTarget ?? undefined,
470
+ );
595
471
  }
596
472
 
597
473
  // Silence the unused-var warning on baseVersion — we keep the value
@@ -533,40 +533,6 @@ complete -F _celilo_completion celilo
533
533
  `;
534
534
  }
535
535
 
536
- /**
537
- * Generate zsh completion script
538
- */
539
- export function generateZshCompletion(): string {
540
- return `#compdef celilo
541
-
542
- _celilo() {
543
- local -a completions
544
- local -a words_array
545
- local current_word
546
-
547
- # Get current word index (CURRENT is 1-based)
548
- current_word=\${CURRENT}
549
-
550
- # Get words from command line
551
- words_array=("\${words[@]}")
552
-
553
- # Call celilo to get completions (returns newline-separated list)
554
- local result
555
- result=$(celilo --get-completions "\${words_array[@]}" "$current_word" 2>/dev/null)
556
-
557
- # Split result into array by newlines
558
- completions=("\${(@f)result}")
559
-
560
- # Provide completions
561
- if (( \${#completions} > 0 )); then
562
- compadd -a completions
563
- fi
564
- }
565
-
566
- compdef _celilo celilo
567
- `;
568
- }
569
-
570
536
  /**
571
537
  * Generate fish completion script.
572
538
  *