@celilo/cli 0.13.0 → 0.13.2

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": "@celilo/cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,6 +74,22 @@
74
74
  "maximum": 4094,
75
75
  "description": "VLAN tag for internal zone (not defaulted; internal is untagged)"
76
76
  },
77
+ "network.secure-mgmt.subnet": {
78
+ "type": "string",
79
+ "pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$",
80
+ "description": "Control-plane subnet CIDR — the network celilo-mgr itself occupies when it does not sit on the internal LAN (not defaulted — discovered at celilo-mgmt install, or recorded by firewall onboarding). Derives the firewall's trusted sources and the internal resolver's split-horizon view for celilo's own traffic."
81
+ },
82
+ "network.secure-mgmt.gateway": {
83
+ "type": "string",
84
+ "pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$",
85
+ "description": "Control-plane gateway IP address (not defaulted — discovered from the default route)"
86
+ },
87
+ "network.secure-mgmt.vlan": {
88
+ "type": "integer",
89
+ "minimum": 1,
90
+ "maximum": 4094,
91
+ "description": "VLAN tag for the control-plane zone (not defaulted)"
92
+ },
77
93
  "dns.primary": {
78
94
  "type": "string",
79
95
  "pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$",
@@ -152,4 +152,49 @@ describe('bumpUpdatesFor', () => {
152
152
  '@celilo/event-bus',
153
153
  ]);
154
154
  });
155
+
156
+ // #399: pins are now written during the version phase from WORKSPACE versions,
157
+ // which run ahead of npm until the publish lands. The publish-phase pass still
158
+ // reads npm-latest, so without a guard it would walk those pins BACKWARDS and
159
+ // ship modules bundling the previous capability code — the exact bundle/repo
160
+ // drift ISS-0104 exists to prevent.
161
+ describe('never downgrades a pin', () => {
162
+ test('an older npm-latest leaves a newer workspace pin alone', () => {
163
+ const pkg: PackageJson = { dependencies: { '@celilo/capabilities': '^0.7.1' } };
164
+ expect(bumpUpdatesFor(pkg, latest({ '@celilo/capabilities': '0.7.0' }))).toEqual([]);
165
+ });
166
+
167
+ test('across a minor and a major', () => {
168
+ expect(
169
+ bumpUpdatesFor(
170
+ { dependencies: { '@celilo/cli': '^0.13.0' } },
171
+ latest({ '@celilo/cli': '0.12.1' }),
172
+ ),
173
+ ).toEqual([]);
174
+ expect(
175
+ bumpUpdatesFor(
176
+ { dependencies: { '@celilo/cli': '^1.0.0' } },
177
+ latest({ '@celilo/cli': '0.99.9' }),
178
+ ),
179
+ ).toEqual([]);
180
+ });
181
+
182
+ test('still bumps forward', () => {
183
+ const updates = bumpUpdatesFor(
184
+ { dependencies: { '@celilo/capabilities': '^0.7.0' } },
185
+ latest({ '@celilo/capabilities': '0.7.1' }),
186
+ );
187
+ expect(updates).toHaveLength(1);
188
+ expect(updates[0].newSpec).toBe('^0.7.1');
189
+ });
190
+
191
+ test('a prerelease suffix does not confuse the comparison', () => {
192
+ expect(
193
+ bumpUpdatesFor(
194
+ { dependencies: { '@celilo/cli': '^0.13.0' } },
195
+ latest({ '@celilo/cli': '0.12.1-alpha.3' }),
196
+ ),
197
+ ).toEqual([]);
198
+ });
199
+ });
155
200
  });
@@ -32,8 +32,8 @@ import type { ConsumerPinItem, PackageJson } from './types';
32
32
  * to current npm-latest. Returns the file paths + per-bucket update
33
33
  * lists; doesn't write anything.
34
34
  */
35
- export function planConsumerPins(): ConsumerPinItem[] {
36
- const latestMap = fetchLatestVersions();
35
+ export function planConsumerPins(versions?: Map<string, string>): ConsumerPinItem[] {
36
+ const latestMap = versions ?? fetchLatestVersions();
37
37
  if (latestMap.size === 0) return [];
38
38
 
39
39
  const externalPaths = readExternalProjectPaths();
@@ -68,6 +68,23 @@ export function planConsumerPins(): ConsumerPinItem[] {
68
68
  * - deps already at the target bare version (operator-class match,
69
69
  * `^1.0.0` and `1.0.0` both count as "at version 1.0.0").
70
70
  */
71
+ /**
72
+ * Compare `major.minor.patch`, ignoring any prerelease suffix. Returns
73
+ * -1 / 0 / 1. Deliberately tiny: pins only ever move between released
74
+ * `@celilo/*` versions, so full semver precedence is not needed.
75
+ */
76
+ function compareVersions(a: string, b: string): number {
77
+ const parse = (v: string) => (v.split('-')[0] ?? '').split('.').map(Number);
78
+ const pa = parse(a);
79
+ const pb = parse(b);
80
+ for (let i = 0; i < 3; i++) {
81
+ const x = pa[i] ?? 0;
82
+ const y = pb[i] ?? 0;
83
+ if (x !== y) return x < y ? -1 : 1;
84
+ }
85
+ return 0;
86
+ }
87
+
71
88
  export function bumpUpdatesFor(
72
89
  pkg: PackageJson,
73
90
  published: Map<string, string>,
@@ -89,6 +106,12 @@ export function bumpUpdatesFor(
89
106
  if (oldSpec.startsWith('workspace:')) continue;
90
107
  if (/^[a-z]+:/.test(oldSpec) && !oldSpec.startsWith('npm:')) continue;
91
108
  if (bareVersion(oldSpec) === newVersion) continue;
109
+ // Never walk a pin backwards. Pins are now written during the version
110
+ // phase from WORKSPACE versions, which are ahead of npm until the publish
111
+ // lands. Without this, the publish-phase pass — which still reads
112
+ // npm-latest — would "helpfully" downgrade every pin the version PR just
113
+ // set, and ship modules bundling the previous capability code.
114
+ if (compareVersions(newVersion, bareVersion(oldSpec)) < 0) continue;
92
115
  const newSpec = withOperator(oldSpec, newVersion);
93
116
  if (newSpec === oldSpec) continue;
94
117
  updates.push({ bucket, depName: name, oldSpec, newSpec });
@@ -355,6 +355,8 @@ _celilo_system_config_keys() {
355
355
  'network.secure.gateway:Secure gateway IP'
356
356
  'network.internal.subnet:Internal subnet'
357
357
  'network.internal.gateway:Internal gateway IP'
358
+ 'network.secure-mgmt.subnet:Control-plane subnet (celilo-mgr own network)'
359
+ 'network.secure-mgmt.gateway:Control-plane gateway IP'
358
360
  'dns.primary:Primary DNS server'
359
361
  'dns.fallback:Fallback DNS servers'
360
362
  'routing.internal_gateway:Internal gateway IP'
@@ -1,4 +1,6 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import type { SystemConfigProperty, SystemConfigSchema } from './system-config-schema-types';
3
5
  import { validateKey, validateValue } from './system-config-validator';
4
6
 
@@ -158,3 +160,26 @@ describe('validateKey', () => {
158
160
  expect(result.error).toContain('network.bridge');
159
161
  });
160
162
  });
163
+
164
+ // The shipped schema is what `celilo system config set` validates against, and it
165
+ // is a hand-maintained enumeration of zones. `secure-mgmt` was added to
166
+ // NETWORK_ZONES, IPAM, zone detection and the manifest schema but NOT here, so the
167
+ // zone existed everywhere except the one place its subnet could be RECORDED — and
168
+ // recording it is what derives firewall trust and the resolver's split-horizon
169
+ // view. It failed only on the live box, at `system config set`.
170
+ describe('shipped system_config.json covers every placement zone', () => {
171
+ const shipped = JSON.parse(
172
+ readFileSync(join(import.meta.dir, '../../schemas/system_config.json'), 'utf-8'),
173
+ ) as SystemConfigSchema;
174
+
175
+ // `external` is deliberately absent: cloud/VPS systems carry their own
176
+ // addressing and celilo allocates nothing for them.
177
+ const ADDRESSABLE_ZONES = ['internal', 'dmz', 'app', 'secure', 'secure-mgmt'] as const;
178
+
179
+ for (const zone of ADDRESSABLE_ZONES) {
180
+ it(`accepts network.${zone}.subnet and .gateway`, () => {
181
+ expect(validateKey(`network.${zone}.subnet`, shipped).valid).toBe(true);
182
+ expect(validateKey(`network.${zone}.gateway`, shipped).valid).toBe(true);
183
+ });
184
+ }
185
+ });