@celilo/cli 0.1.3 → 0.1.5

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.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@aws-sdk/client-s3": "^3.1024.0",
49
49
  "@clack/prompts": "^1.1.0",
50
- "@celilo/capabilities": "^0.1.1",
50
+ "@celilo/capabilities": "^0.1.2",
51
51
  "ajv": "^8.18.0",
52
52
  "drizzle-orm": "^0.36.4",
53
53
  "tar": "^7.5.10",
@@ -99,14 +99,14 @@ describe('generateHostVarsYaml', () => {
99
99
  const vars = {
100
100
  vmid: 2110,
101
101
  hostname: 'iot',
102
- container_ip: '192.168.0.110/24',
102
+ target_ip: '192.168.0.110/24',
103
103
  };
104
104
 
105
105
  const result = generateHostVarsYaml(vars);
106
106
 
107
107
  expect(result).toContain('vmid: 2110');
108
108
  expect(result).toContain('hostname: iot');
109
- expect(result).toContain('container_ip: 192.168.0.110/24');
109
+ expect(result).toContain('target_ip: 192.168.0.110/24');
110
110
  });
111
111
 
112
112
  test('generates YAML for arrays', () => {
@@ -289,7 +289,7 @@ describe('Database integration', () => {
289
289
  { moduleId: 'homebridge', key: 'hostname', value: 'iot', valueJson: null },
290
290
  {
291
291
  moduleId: 'homebridge',
292
- key: 'container_ip',
292
+ key: 'target_ip',
293
293
  value: '192.168.0.110/24',
294
294
  valueJson: null,
295
295
  },
@@ -301,7 +301,7 @@ describe('Database integration', () => {
301
301
 
302
302
  expect(vars.vmid).toBe(2110);
303
303
  expect(vars.hostname).toBe('iot');
304
- expect(vars.container_ip).toBe('192.168.0.110/24');
304
+ expect(vars.target_ip).toBe('192.168.0.110/24');
305
305
  expect(vars.cores).toBe(1);
306
306
  });
307
307
 
@@ -404,7 +404,7 @@ describe('Database integration', () => {
404
404
  db.insert(moduleConfigs)
405
405
  .values([
406
406
  { moduleId: 'homebridge', key: 'hostname', value: 'iot' },
407
- { moduleId: 'homebridge', key: 'container_ip', value: '192.168.0.110/24' },
407
+ { moduleId: 'homebridge', key: 'target_ip', value: '192.168.0.110/24' },
408
408
  ])
409
409
  .run();
410
410
 
@@ -446,7 +446,7 @@ describe('Database integration', () => {
446
446
  db.insert(moduleConfigs)
447
447
  .values([
448
448
  { moduleId: 'test', key: 'hostname', value: 'iot' },
449
- // Missing container_ip or vps_ip for ansible_host
449
+ // Missing target_ip or vps_ip for ansible_host
450
450
  ])
451
451
  .run();
452
452
 
@@ -259,13 +259,11 @@ export function extractInventoryHost(moduleId: string, db: DbClient): InventoryH
259
259
  }
260
260
 
261
261
  // Auto-derive ansible_host from infrastructure variables
262
- // Priority: container_ip > ip.primary > vps_ip (for backward compatibility)
263
- if (moduleConfig.container_ip) {
264
- const slashIndex = moduleConfig.container_ip.indexOf('/');
262
+ // Priority: target_ip > ip.primary > vps_ip (for backward compatibility)
263
+ if (moduleConfig.target_ip) {
264
+ const slashIndex = moduleConfig.target_ip.indexOf('/');
265
265
  derived['inventory.ansible_host'] =
266
- slashIndex === -1
267
- ? moduleConfig.container_ip
268
- : moduleConfig.container_ip.slice(0, slashIndex);
266
+ slashIndex === -1 ? moduleConfig.target_ip : moduleConfig.target_ip.slice(0, slashIndex);
269
267
  } else if (moduleConfig['ip.primary']) {
270
268
  derived['inventory.ansible_host'] = moduleConfig['ip.primary'];
271
269
  } else if (moduleConfig.vps_ip) {
@@ -109,7 +109,7 @@ describe('publishStaticSite — clientConfig injection', () => {
109
109
  moduleId: 'lunacycle',
110
110
  logger: noopLogger,
111
111
  config: {
112
- container_ip: '10.0.10.20/24',
112
+ target_ip: '10.0.10.20/24',
113
113
  hostname: 'www',
114
114
  primary_domain: 'example.com',
115
115
  email: 'admin@example.com',
@@ -150,7 +150,7 @@ describe('publishStaticSite — clientConfig injection', () => {
150
150
  moduleId: 'lunacycle',
151
151
  logger: noopLogger,
152
152
  config: {
153
- container_ip: '10.0.10.20/24',
153
+ target_ip: '10.0.10.20/24',
154
154
  hostname: 'www',
155
155
  primary_domain: 'example.com',
156
156
  email: 'admin@example.com',
@@ -174,7 +174,7 @@ describe('publishStaticSite — clientConfig injection', () => {
174
174
  moduleId: 'lunacycle',
175
175
  logger: noopLogger,
176
176
  config: {
177
- container_ip: '10.0.10.20/24',
177
+ target_ip: '10.0.10.20/24',
178
178
  hostname: 'www',
179
179
  primary_domain: 'example.com',
180
180
  email: 'admin@example.com',
@@ -201,7 +201,7 @@ describe('publishStaticSite — clientConfig injection', () => {
201
201
  moduleId: 'lunacycle',
202
202
  logger: noopLogger,
203
203
  config: {
204
- container_ip: '10.0.10.20/24',
204
+ target_ip: '10.0.10.20/24',
205
205
  hostname: 'www',
206
206
  primary_domain: 'example.com',
207
207
  email: 'admin@example.com',
@@ -225,7 +225,7 @@ describe('publishStaticSite — clientConfig injection', () => {
225
225
  moduleId: 'lunacycle',
226
226
  logger: noopLogger,
227
227
  config: {
228
- container_ip: '10.0.10.20/24',
228
+ target_ip: '10.0.10.20/24',
229
229
  hostname: 'www',
230
230
  primary_domain: 'example.com',
231
231
  email: 'admin@example.com',
@@ -261,7 +261,7 @@ describe('registerReverseProxy', () => {
261
261
  moduleId: 'lunacycle',
262
262
  logger: noopLogger,
263
263
  config: {
264
- container_ip: '10.0.10.20/24',
264
+ target_ip: '10.0.10.20/24',
265
265
  hostname: 'www',
266
266
  primary_domain: 'example.com',
267
267
  email: 'admin@example.com',
@@ -294,7 +294,7 @@ describe('registerReverseProxy', () => {
294
294
  moduleId: 'lunacycle',
295
295
  logger: noopLogger,
296
296
  config: {
297
- container_ip: '10.0.10.20/24',
297
+ target_ip: '10.0.10.20/24',
298
298
  hostname: 'www',
299
299
  primary_domain: 'example.com',
300
300
  email: 'admin@example.com',
@@ -337,7 +337,7 @@ describe('auto-logging — end-to-end through createPublicWeb', () => {
337
337
  moduleId: 'lunacycle',
338
338
  logger,
339
339
  config: {
340
- container_ip: '10.0.10.20/24',
340
+ target_ip: '10.0.10.20/24',
341
341
  hostname: 'www',
342
342
  primary_domain: 'example.com',
343
343
  email: 'admin@example.com',
@@ -368,7 +368,7 @@ describe('auto-logging — end-to-end through createPublicWeb', () => {
368
368
  moduleId: 'lunacycle',
369
369
  logger,
370
370
  config: {
371
- container_ip: '10.0.10.20/24',
371
+ target_ip: '10.0.10.20/24',
372
372
  hostname: 'www',
373
373
  primary_domain: 'example.com',
374
374
  email: 'admin@example.com',
@@ -400,7 +400,7 @@ describe('auto-logging — end-to-end through createPublicWeb', () => {
400
400
  moduleId: 'lunacycle',
401
401
  logger,
402
402
  config: {
403
- container_ip: '10.0.10.20/24',
403
+ target_ip: '10.0.10.20/24',
404
404
  hostname: 'www',
405
405
  primary_domain: 'example.com',
406
406
  email: 'admin@example.com',
@@ -432,7 +432,7 @@ describe('auto-logging — end-to-end through createPublicWeb', () => {
432
432
  moduleId: 'lunacycle',
433
433
  logger,
434
434
  config: {
435
- container_ip: '10.0.10.20/24',
435
+ target_ip: '10.0.10.20/24',
436
436
  hostname: 'www',
437
437
  primary_domain: 'example.com',
438
438
  email: 'admin@example.com',
@@ -43,7 +43,7 @@ describe('Capability Registration', () => {
43
43
  version: '1.0.0',
44
44
  data: {
45
45
  server: {
46
- ip: '$self:container_ip',
46
+ ip: '$self:target_ip',
47
47
  port: 443,
48
48
  },
49
49
  },
@@ -65,7 +65,7 @@ describe('Capability Registration', () => {
65
65
  // Variables are preserved, not resolved
66
66
  expect(result).toEqual({
67
67
  server: {
68
- ip: '$self:container_ip',
68
+ ip: '$self:target_ip',
69
69
  port: 443,
70
70
  },
71
71
  });
@@ -177,7 +177,7 @@ describe('Capability Registration', () => {
177
177
  name: 'test_capability',
178
178
  version: '1.0.0',
179
179
  data: {
180
- variable: '$self:container_ip',
180
+ variable: '$self:target_ip',
181
181
  literal: 'not-a-variable',
182
182
  with_dollar: '$100',
183
183
  empty: '',
@@ -198,7 +198,7 @@ describe('Capability Registration', () => {
198
198
  const result = buildCapabilityData(capability, manifest);
199
199
 
200
200
  expect(result).toEqual({
201
- variable: '$self:container_ip',
201
+ variable: '$self:target_ip',
202
202
  literal: 'not-a-variable',
203
203
  with_dollar: '$100',
204
204
  empty: '',
@@ -337,7 +337,7 @@ describe('Capability Registration', () => {
337
337
  version: '1.0.0',
338
338
  data: {
339
339
  server: {
340
- ip: '$self:container_ip',
340
+ ip: '$self:target_ip',
341
341
  },
342
342
  },
343
343
  };
@@ -360,7 +360,7 @@ describe('Capability Registration', () => {
360
360
  (result as any).server.ip = 'modified';
361
361
 
362
362
  // Original should be unchanged
363
- expect(capability.data.server.ip).toBe('$self:container_ip');
363
+ expect(capability.data.server.ip).toBe('$self:target_ip');
364
364
  });
365
365
  });
366
366
 
@@ -170,7 +170,7 @@ describe('Well-Known Capabilities Registry', () => {
170
170
  expect(schema).toEqual({
171
171
  server: {
172
172
  ip: {
173
- primary: '$self:container_ip',
173
+ primary: '$self:target_ip',
174
174
  },
175
175
  port: 443,
176
176
  },
@@ -196,7 +196,7 @@ describe('Well-Known Capabilities Registry', () => {
196
196
  expect(schema).toEqual({
197
197
  server: {
198
198
  ip: {
199
- primary: '$self:container_ip',
199
+ primary: '$self:target_ip',
200
200
  },
201
201
  port: 9000,
202
202
  },
@@ -41,7 +41,7 @@ export const WELL_KNOWN_CAPABILITIES: Record<string, WellKnownCapability> = {
41
41
  data_schema: {
42
42
  server: {
43
43
  ip: {
44
- primary: '$self:container_ip',
44
+ primary: '$self:target_ip',
45
45
  },
46
46
  port: 443,
47
47
  },
@@ -76,7 +76,7 @@ export const WELL_KNOWN_CAPABILITIES: Record<string, WellKnownCapability> = {
76
76
  data_schema: {
77
77
  server: {
78
78
  ip: {
79
- primary: '$self:container_ip',
79
+ primary: '$self:target_ip',
80
80
  },
81
81
  port: 53,
82
82
  },
@@ -102,7 +102,7 @@ export const WELL_KNOWN_CAPABILITIES: Record<string, WellKnownCapability> = {
102
102
  data_schema: {
103
103
  server: {
104
104
  ip: {
105
- primary: '$self:container_ip',
105
+ primary: '$self:target_ip',
106
106
  },
107
107
  port: 9000,
108
108
  },
@@ -121,12 +121,12 @@ export const WELL_KNOWN_CAPABILITIES: Record<string, WellKnownCapability> = {
121
121
  data_schema: {
122
122
  server: {
123
123
  ip: {
124
- primary: '$self:container_ip',
124
+ primary: '$self:target_ip',
125
125
  },
126
126
  port: '$self:port', // Database-specific port
127
127
  },
128
128
  connection: {
129
- host: '$self:container_ip',
129
+ host: '$self:target_ip',
130
130
  port: '$self:port',
131
131
  name: '$self:database_name',
132
132
  },
@@ -68,7 +68,7 @@ export async function handleModuleShowConfig(args: string[]): Promise<CommandRes
68
68
  if (
69
69
  key.startsWith('inventory.') ||
70
70
  key === 'vmid' ||
71
- key === 'container_ip' ||
71
+ key === 'target_ip' ||
72
72
  key === 'gateway' ||
73
73
  key === 'vlan' ||
74
74
  key === 'subnet' ||
@@ -121,7 +121,7 @@ describe('Database Schema', () => {
121
121
  // Insert config
122
122
  const newConfig: NewModuleConfig = {
123
123
  moduleId: 'homebridge',
124
- key: 'container_ip',
124
+ key: 'target_ip',
125
125
  value: '192.168.0.50',
126
126
  };
127
127
 
@@ -129,7 +129,7 @@ describe('Database Schema', () => {
129
129
 
130
130
  expect(result).toBeDefined();
131
131
  expect(result.moduleId).toBe('homebridge');
132
- expect(result.key).toBe('container_ip');
132
+ expect(result.key).toBe('target_ip');
133
133
  expect(result.value).toBe('192.168.0.50');
134
134
  });
135
135
 
@@ -206,7 +206,7 @@ describe('Database Schema', () => {
206
206
  // Insert config
207
207
  const newConfig: NewModuleConfig = {
208
208
  moduleId: 'homebridge',
209
- key: 'container_ip',
209
+ key: 'target_ip',
210
210
  value: '192.168.0.50',
211
211
  };
212
212
  db.insert(moduleConfigs).values(newConfig).run();
@@ -124,22 +124,6 @@ export async function loadCapabilityFunctions(
124
124
  const providerSecrets = await loadModuleSecrets(provider.moduleId, masterKey, db);
125
125
  const routeOps = buildRouteOps(db);
126
126
 
127
- // Inject machine IP if module is deployed to a machine (not in moduleConfigs table)
128
- if (!providerConfig.container_ip) {
129
- const infra = db
130
- .select()
131
- .from(moduleInfrastructure)
132
- .where(eq(moduleInfrastructure.moduleId, provider.moduleId))
133
- .get();
134
- if (infra?.machineId) {
135
- const machine = db.select().from(machines).where(eq(machines.id, infra.machineId)).get();
136
- if (machine) {
137
- providerConfig.container_ip = machine.ipAddress;
138
- debugLog(`public_web: injected container_ip=${machine.ipAddress} from machine`);
139
- }
140
- }
141
- }
142
-
143
127
  try {
144
128
  // createPublicWeb internally applies wrapWithLogging using the
145
129
  // logger we pass in here, so the consumer doesn't need to wrap
@@ -308,7 +292,11 @@ function buildCapabilityInterface(
308
292
  }
309
293
 
310
294
  /**
311
- * Load all config values for a module
295
+ * Load all config values for a module.
296
+ *
297
+ * Injects `target_ip` from the deployment machine when the module was deployed
298
+ * to an existing machine (IPAM writes `target_ip` to moduleConfigs for
299
+ * container deploys, but machine deploys skip that path).
312
300
  */
313
301
  async function loadModuleConfig(moduleId: string, db: DbClient): Promise<Record<string, unknown>> {
314
302
  const configs = db.select().from(moduleConfigs).where(eq(moduleConfigs.moduleId, moduleId)).all();
@@ -317,6 +305,21 @@ async function loadModuleConfig(moduleId: string, db: DbClient): Promise<Record<
317
305
  for (const c of configs) {
318
306
  result[c.key] = c.valueJson ? JSON.parse(c.valueJson) : c.value;
319
307
  }
308
+
309
+ if (!result.target_ip) {
310
+ const infra = db
311
+ .select()
312
+ .from(moduleInfrastructure)
313
+ .where(eq(moduleInfrastructure.moduleId, moduleId))
314
+ .get();
315
+ if (infra?.machineId) {
316
+ const machine = db.select().from(machines).where(eq(machines.id, infra.machineId)).get();
317
+ if (machine) {
318
+ result.target_ip = machine.ipAddress;
319
+ }
320
+ }
321
+ }
322
+
320
323
  return result;
321
324
  }
322
325
 
@@ -286,7 +286,7 @@ describe('defineCapabilityFunction', () => {
286
286
  const factory = defineCapabilityFunction({
287
287
  capability: 'idp',
288
288
  handler: ({ config, secrets }) => {
289
- expect(config).toEqual({ container_ip: '10.0.0.5' });
289
+ expect(config).toEqual({ target_ip: '10.0.0.5' });
290
290
  expect(secrets.token).toBe('abc');
291
291
  return {
292
292
  async create_oidc_client(
@@ -310,7 +310,7 @@ describe('defineCapabilityFunction', () => {
310
310
  });
311
311
 
312
312
  const methods = factory({
313
- config: { container_ip: '10.0.0.5' },
313
+ config: { target_ip: '10.0.0.5' },
314
314
  secrets: { token: 'abc' },
315
315
  logger: makeLogger(),
316
316
  });
@@ -40,7 +40,7 @@ function createTestManifest(overrides?: Partial<ModuleManifest>): ModuleManifest
40
40
  source: 'user',
41
41
  },
42
42
  {
43
- name: 'container_ip',
43
+ name: 'target_ip',
44
44
  type: 'string',
45
45
  required: false,
46
46
  source: 'user',
@@ -68,7 +68,7 @@ function pathExistsInManifest(manifest: ModuleManifest, path: string): boolean {
68
68
  */
69
69
  const AUTO_ALLOCATED_VARIABLES = new Set([
70
70
  'vmid', // Auto-allocated by IPAM (container-based modules)
71
- 'container_ip', // Auto-allocated by IPAM (container-based modules)
71
+ 'target_ip', // Auto-allocated by IPAM or injected from machine infrastructure
72
72
  'vlan', // Auto-derived from zone configuration
73
73
  'gateway', // Auto-derived from zone configuration
74
74
  'target_node', // Can be auto-derived from system config
@@ -894,7 +894,7 @@ describe('validateVariableSources', () => {
894
894
  variables: {
895
895
  owns: [
896
896
  {
897
- name: 'container_ip',
897
+ name: 'target_ip',
898
898
  type: 'string' as const,
899
899
  required: true,
900
900
  source: 'user' as const,
@@ -1,6 +1,6 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { cpSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
3
- import { readFile, readdir, unlink, writeFile } from 'node:fs/promises';
3
+ import { readFile, readdir, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { basename, join, relative } from 'node:path';
6
6
  import { create as tarCreate } from 'tar';
@@ -140,17 +140,44 @@ export async function buildModule(options: ModuleBuildOptions): Promise<ModuleBu
140
140
  return { success: false, error: dirError };
141
141
  }
142
142
 
143
- // Copy source to a temp directory so the build doesn't pollute the
144
- // source tree and avoids workspace resolution issues (e.g., a
145
- // monorepo's root package.json triggering bun workspace linking).
143
+ // Copy source to a temp dir for building. Strategy:
144
+ // - If the source has a package.json, use `bun pm pack` to respect the
145
+ // `files` field (or .npmignore), copying only what the build needs.
146
+ // Avoids copying node_modules, build artifacts, git history.
147
+ // - If no package.json (simple modules, test fixtures), fall back to
148
+ // recursive copy with EXCLUDE_PATTERNS filter.
146
149
  const buildDir = mkdtempSync(join(tmpdir(), 'celilo-package-'));
150
+ const hasPackageJson = existsSync(join(sourceDir, 'package.json'));
147
151
 
148
152
  try {
149
- console.log('Copying source to build directory...');
150
- cpSync(sourceDir, buildDir, {
151
- recursive: true,
152
- filter: (src) => !shouldExclude(relative(sourceDir, src)),
153
- });
153
+ if (hasPackageJson) {
154
+ console.log('Staging source for build (via bun pm pack)...');
155
+ try {
156
+ execSync(`bun pm pack --destination ${buildDir}`, {
157
+ cwd: sourceDir,
158
+ stdio: 'pipe',
159
+ timeout: 60_000,
160
+ });
161
+ const tarballs = execSync(`ls ${buildDir}/*.tgz`, { encoding: 'utf-8' }).trim().split('\n');
162
+ if (tarballs.length === 0) throw new Error('bun pm pack produced no tarball');
163
+ execSync(`tar -xzf ${tarballs[0]} --strip-components=1 -C ${buildDir}`, {
164
+ timeout: 60_000,
165
+ });
166
+ rmSync(tarballs[0], { force: true });
167
+ } catch (err) {
168
+ const errMsg = err instanceof Error ? err.message : String(err);
169
+ return {
170
+ success: false,
171
+ error: `Failed to stage source for build: ${errMsg}\n\nTip: Add a "files" field to your package.json listing the files/directories needed for the build.`,
172
+ };
173
+ }
174
+ } else {
175
+ console.log('Staging source for build (no package.json, using file copy)...');
176
+ cpSync(sourceDir, buildDir, {
177
+ recursive: true,
178
+ filter: (src) => !shouldExclude(relative(sourceDir, src)),
179
+ });
180
+ }
154
181
 
155
182
  // Read manifest to get module ID
156
183
  const manifestPath = join(buildDir, 'manifest.yml');
@@ -230,7 +257,7 @@ export async function buildModule(options: ModuleBuildOptions): Promise<ModuleBu
230
257
  error: `Failed to build module: ${error instanceof Error ? error.message : 'Unknown error'}`,
231
258
  };
232
259
  } finally {
233
- // Clean up temp directory
260
+ // Clean up temp build directory
234
261
  rmSync(buildDir, { recursive: true, force: true });
235
262
  }
236
263
  }
@@ -126,15 +126,15 @@ export async function extractTargetHost(
126
126
  const hostname = configMap.get('hostname') || moduleId;
127
127
 
128
128
  // Extract IP - support infrastructure-derived variables
129
- // Priority: container_ip > ip.primary > vps_ip (backward compatibility)
129
+ // Priority: target_ip > ip.primary > vps_ip (backward compatibility)
130
130
  let ip = '';
131
- const containerIp = configMap.get('container_ip');
131
+ const targetIp = configMap.get('target_ip');
132
132
  const ipPrimary = configMap.get('ip.primary');
133
133
  const vpsIp = configMap.get('vps_ip');
134
134
 
135
- if (containerIp) {
136
- // Container IP format: "10.0.10.10/24" - extract just IP
137
- ip = containerIp.split('/')[0];
135
+ if (targetIp) {
136
+ // Target IP format can be "10.0.10.10/24" - extract just IP
137
+ ip = targetIp.split('/')[0];
138
138
  } else if (ipPrimary) {
139
139
  // Infrastructure-derived IP (already plain format)
140
140
  ip = ipPrimary;
@@ -38,15 +38,15 @@ async function getModuleHostAndIp(
38
38
 
39
39
  if (!hostnameConfig?.value) return null;
40
40
 
41
- // Get IP: try container_ip first, then machine IP
42
- const containerIpConfig = db
41
+ // Get IP: try target_ip first, then machine IP
42
+ const targetIpConfig = db
43
43
  .select()
44
44
  .from(moduleConfigs)
45
45
  .where(eq(moduleConfigs.moduleId, moduleId))
46
46
  .all()
47
- .find((c) => c.key === 'container_ip');
47
+ .find((c) => c.key === 'target_ip');
48
48
 
49
- let ip = containerIpConfig?.value;
49
+ let ip = targetIpConfig?.value;
50
50
 
51
51
  if (!ip) {
52
52
  // Try machine IP from infrastructure assignment
@@ -80,7 +80,7 @@ export async function runModuleHealthCheck(
80
80
  const machine = await getMachine(infraRecord.machineId);
81
81
  if (machine) {
82
82
  configMap['ip.primary'] = machine.ipAddress;
83
- configMap.container_ip = machine.ipAddress;
83
+ configMap.target_ip = machine.ipAddress;
84
84
  }
85
85
  }
86
86
 
@@ -277,7 +277,7 @@ describe('resolveInfrastructureVariables - Proxmox Container Service', () => {
277
277
  },
278
278
  {
279
279
  moduleId,
280
- key: 'container_ip',
280
+ key: 'target_ip',
281
281
  value: '10.0.10.5',
282
282
  valueJson: null,
283
283
  },
@@ -128,10 +128,10 @@ export async function resolveInfrastructureVariables(
128
128
  } else if (service.providerName === 'proxmox') {
129
129
  // Proxmox - use IPAM allocation + service provider config
130
130
  const vmid = await getModuleConfig(moduleId, 'vmid', db);
131
- const containerIp = await getModuleConfig(moduleId, 'container_ip', db);
131
+ const targetIp = await getModuleConfig(moduleId, 'target_ip', db);
132
132
  const hostname = (await getModuleConfig(moduleId, 'hostname', db)) || moduleId;
133
133
 
134
- if (!vmid || !containerIp) {
134
+ if (!vmid || !targetIp) {
135
135
  throw new Error(
136
136
  `IPAM allocation not found for module ${moduleId}. ` +
137
137
  `Run 'celilo module generate ${moduleId}' first.`,
@@ -143,7 +143,7 @@ export async function resolveInfrastructureVariables(
143
143
 
144
144
  properties = extractProxmoxProperties(
145
145
  Number.parseInt(vmid, 10),
146
- containerIp,
146
+ targetIp,
147
147
  hostname,
148
148
  providerConfig,
149
149
  );
@@ -741,13 +741,13 @@ export async function deployModule(
741
741
  installConfigMap[c.key] = c.valueJson ? JSON.parse(c.valueJson) : c.value;
742
742
  }
743
743
 
744
- // Inject machine IP into hook config for machine deployments
744
+ // Inject target IP into hook config — works for both machine and container deploys
745
745
  if (machineId) {
746
746
  const { getMachine } = await import('./machine-pool');
747
747
  const deployMachine = await getMachine(machineId);
748
748
  if (deployMachine) {
749
749
  installConfigMap['ip.primary'] = deployMachine.ipAddress;
750
- installConfigMap.container_ip = deployMachine.ipAddress;
750
+ installConfigMap.target_ip = deployMachine.ipAddress;
751
751
  }
752
752
  }
753
753
 
@@ -31,7 +31,7 @@ interface TerraformState {
31
31
  }
32
32
 
33
33
  /**
34
- * Ensure module_configs has vmid and container_ip from Terraform state
34
+ * Ensure module_configs has vmid and target_ip from Terraform state
35
35
  * This recovers from state drift scenarios where container was deleted/recreated
36
36
  *
37
37
  * @param moduleId - Module identifier
@@ -43,7 +43,7 @@ export async function ensureProxmoxConfigFromState(
43
43
  terraformDir: string,
44
44
  db: DbClient,
45
45
  ): Promise<void> {
46
- // Check if vmid and container_ip already exist
46
+ // Check if vmid and target_ip already exist
47
47
  const existingVmid = await db
48
48
  .select()
49
49
  .from(moduleConfigs)
@@ -53,7 +53,7 @@ export async function ensureProxmoxConfigFromState(
53
53
  const existingContainerIp = await db
54
54
  .select()
55
55
  .from(moduleConfigs)
56
- .where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, 'container_ip')))
56
+ .where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, 'target_ip')))
57
57
  .get();
58
58
 
59
59
  // If both exist, no recovery needed
@@ -104,7 +104,7 @@ export async function ensureProxmoxConfigFromState(
104
104
  }
105
105
 
106
106
  // Store in module_configs (recovery)
107
- log.warn(' Recovering vmid and container_ip from Terraform state...');
107
+ log.warn(' Recovering vmid and target_ip from Terraform state...');
108
108
 
109
109
  if (!existingVmid) {
110
110
  await db
@@ -126,7 +126,7 @@ export async function ensureProxmoxConfigFromState(
126
126
  .insert(moduleConfigs)
127
127
  .values({
128
128
  moduleId,
129
- key: 'container_ip',
129
+ key: 'target_ip',
130
130
  value: containerIp,
131
131
  })
132
132
  .onConflictDoUpdate({
@@ -136,5 +136,5 @@ export async function ensureProxmoxConfigFromState(
136
136
  .run();
137
137
  }
138
138
 
139
- log.success(` Recovered: vmid=${vmid}, container_ip=${containerIp}`);
139
+ log.success(` Recovered: vmid=${vmid}, target_ip=${containerIp}`);
140
140
  }
@@ -321,7 +321,7 @@ describe('Template Generator', () => {
321
321
 
322
322
  db.insert(moduleConfigs)
323
323
  .values([
324
- { moduleId: 'test-module', key: 'container_ip', value: '192.168.0.50' },
324
+ { moduleId: 'test-module', key: 'target_ip', value: '192.168.0.50' },
325
325
  { moduleId: 'test-module', key: 'hostname', value: 'test' },
326
326
  ])
327
327
  .run();
@@ -340,7 +340,7 @@ describe('Template Generator', () => {
340
340
  resource "proxmox_lxc" "container" {
341
341
  hostname = "$self:hostname"
342
342
  network {
343
- ip = "$self:container_ip/24"
343
+ ip = "$self:target_ip/24"
344
344
  gateway = "$system:management.ip"
345
345
  }
346
346
  }
@@ -621,7 +621,7 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
621
621
  // biome-ignore lint/style/noNonNullAssertion: value is NOT NULL in schema, guaranteed to exist
622
622
  context.selfConfig.vmid = String(vmidConfig.value!);
623
623
  // biome-ignore lint/style/noNonNullAssertion: value is NOT NULL in schema, guaranteed to exist
624
- context.selfConfig.container_ip = ipConfig.value!;
624
+ context.selfConfig.target_ip = ipConfig.value!;
625
625
  }
626
626
 
627
627
  // Add infrastructure properties to context (target_node, lxc_template, storage)
@@ -166,7 +166,7 @@ describe('Variable Context', () => {
166
166
 
167
167
  db.insert(moduleConfigs)
168
168
  .values([
169
- { moduleId: 'homebridge', key: 'container_ip', value: '192.168.0.50' },
169
+ { moduleId: 'homebridge', key: 'target_ip', value: '192.168.0.50' },
170
170
  { moduleId: 'homebridge', key: 'hostname', value: 'homebridge' },
171
171
  ])
172
172
  .run();
@@ -174,7 +174,7 @@ describe('Variable Context', () => {
174
174
  const context = await buildResolutionContext('homebridge', db);
175
175
 
176
176
  expect(context.moduleId).toBe('homebridge');
177
- expect(context.selfConfig.container_ip).toBe('192.168.0.50');
177
+ expect(context.selfConfig.target_ip).toBe('192.168.0.50');
178
178
  expect(context.selfConfig.hostname).toBe('homebridge');
179
179
  // Auto-derived variables
180
180
  expect(context.selfConfig['inventory.ansible_host']).toBe('192.168.0.50');
@@ -262,7 +262,7 @@ describe('Variable Context', () => {
262
262
  );
263
263
 
264
264
  db.insert(moduleConfigs)
265
- .values({ moduleId: 'caddy', key: 'container_ip', value: '10.0.20.10' })
265
+ .values({ moduleId: 'caddy', key: 'target_ip', value: '10.0.20.10' })
266
266
  .run();
267
267
 
268
268
  db.insert(secrets)
@@ -290,7 +290,7 @@ describe('Variable Context', () => {
290
290
 
291
291
  const context = await buildResolutionContext('caddy', db);
292
292
 
293
- expect(context.selfConfig.container_ip).toBe('10.0.20.10');
293
+ expect(context.selfConfig.target_ip).toBe('10.0.20.10');
294
294
  expect(context.secrets.ssl_cert).toBe('cert_data');
295
295
  expect(context.capabilities.dns_external).toBeDefined();
296
296
  expect(context.systemConfig['dns.primary']).toBe('192.168.0.1');
@@ -303,7 +303,7 @@ describe('Variable Context', () => {
303
303
  // Should have auto-derived inventory variables
304
304
  expect(context.selfConfig['inventory.ansible_user']).toBe('root');
305
305
  expect(context.selfConfig['inventory.groups']).toBe('empty-module');
306
- // Should not have ansible_host (no container_ip)
306
+ // Should not have ansible_host (no target_ip)
307
307
  expect(context.selfConfig['inventory.ansible_host']).toBeUndefined();
308
308
  expect(context.secrets).toEqual({});
309
309
  expect(context.capabilities).toEqual({});
@@ -367,18 +367,18 @@ describe('Variable Context', () => {
367
367
  expect(context.selfConfig['requires.machine.memory']).toBeUndefined();
368
368
  });
369
369
 
370
- test('should auto-derive inventory variables from container_ip', async () => {
370
+ test('should auto-derive inventory variables from target_ip', async () => {
371
371
  db.$client.run(
372
372
  `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('test-module', 'Test', '1.0.0', '/path', '{}')`,
373
373
  );
374
374
 
375
375
  db.insert(moduleConfigs)
376
- .values({ moduleId: 'test-module', key: 'container_ip', value: '10.0.10.10/24' })
376
+ .values({ moduleId: 'test-module', key: 'target_ip', value: '10.0.10.10/24' })
377
377
  .run();
378
378
 
379
379
  const context = await buildResolutionContext('test-module', db);
380
380
 
381
- // Should strip CIDR from container_ip
381
+ // Should strip CIDR from target_ip
382
382
  expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.10.10');
383
383
  expect(context.selfConfig['inventory.ansible_user']).toBe('root');
384
384
  expect(context.selfConfig['inventory.groups']).toBe('test-module');
@@ -391,7 +391,7 @@ describe('Variable Context', () => {
391
391
 
392
392
  db.insert(moduleConfigs)
393
393
  .values([
394
- { moduleId: 'custom', key: 'container_ip', value: '10.0.20.10' },
394
+ { moduleId: 'custom', key: 'target_ip', value: '10.0.20.10' },
395
395
  { moduleId: 'custom', key: 'inventory.ansible_user', value: 'admin' },
396
396
  ])
397
397
  .run();
@@ -403,13 +403,13 @@ describe('Variable Context', () => {
403
403
  expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.20.10');
404
404
  });
405
405
 
406
- test('should handle container_ip without CIDR', async () => {
406
+ test('should handle target_ip without CIDR', async () => {
407
407
  db.$client.run(
408
408
  `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('no-cidr', 'No CIDR', '1.0.0', '/path', '{}')`,
409
409
  );
410
410
 
411
411
  db.insert(moduleConfigs)
412
- .values({ moduleId: 'no-cidr', key: 'container_ip', value: '192.168.1.100' })
412
+ .values({ moduleId: 'no-cidr', key: 'target_ip', value: '192.168.1.100' })
413
413
  .run();
414
414
 
415
415
  const context = await buildResolutionContext('no-cidr', db);
@@ -418,8 +418,8 @@ describe('Variable Context', () => {
418
418
  expect(context.selfConfig['inventory.ansible_host']).toBe('192.168.1.100');
419
419
  });
420
420
 
421
- test('should auto-allocate IPAM resources when module declares vmid and container_ip', async () => {
422
- // Create module with manifest that declares vmid and container_ip
421
+ test('should auto-allocate IPAM resources when module declares vmid and target_ip', async () => {
422
+ // Create module with manifest that declares vmid and target_ip
423
423
  db.insert(modules)
424
424
  .values({
425
425
  id: 'auto-module',
@@ -433,7 +433,7 @@ describe('Variable Context', () => {
433
433
  variables: {
434
434
  owns: [
435
435
  { name: 'vmid', type: 'integer', required: true, source: 'user' },
436
- { name: 'container_ip', type: 'string', required: true, source: 'user' },
436
+ { name: 'target_ip', type: 'string', required: true, source: 'user' },
437
437
  ],
438
438
  },
439
439
  requires: {
@@ -447,9 +447,9 @@ describe('Variable Context', () => {
447
447
 
448
448
  const context = await buildResolutionContext('auto-module', db);
449
449
 
450
- // Should have auto-allocated vmid and container_ip
450
+ // Should have auto-allocated vmid and target_ip
451
451
  expect(context.selfConfig.vmid).toBe('2100');
452
- expect(context.selfConfig.container_ip).toBe('10.0.10.10/24');
452
+ expect(context.selfConfig.target_ip).toBe('10.0.10.10/24');
453
453
 
454
454
  // Verify allocation persisted to database
455
455
  const allocations = await db.select().from(ipAllocations).all();
@@ -465,7 +465,7 @@ describe('Variable Context', () => {
465
465
  .where(eq(moduleConfigs.moduleId, 'auto-module'))
466
466
  .all();
467
467
  const vmidConfig = configs.find((c) => c.key === 'vmid');
468
- const ipConfig = configs.find((c) => c.key === 'container_ip');
468
+ const ipConfig = configs.find((c) => c.key === 'target_ip');
469
469
  expect(vmidConfig?.value).toBe('2100');
470
470
  expect(ipConfig?.value).toBe('10.0.10.10/24');
471
471
  });
@@ -485,7 +485,7 @@ describe('Variable Context', () => {
485
485
  variables: {
486
486
  owns: [
487
487
  { name: 'vmid', type: 'integer', required: true, source: 'user' },
488
- { name: 'container_ip', type: 'string', required: true, source: 'user' },
488
+ { name: 'target_ip', type: 'string', required: true, source: 'user' },
489
489
  ],
490
490
  },
491
491
  },
@@ -506,14 +506,14 @@ describe('Variable Context', () => {
506
506
 
507
507
  // Should reuse existing allocation
508
508
  expect(context.selfConfig.vmid).toBe('2150');
509
- expect(context.selfConfig.container_ip).toBe('10.0.10.50/24');
509
+ expect(context.selfConfig.target_ip).toBe('10.0.10.50/24');
510
510
 
511
511
  // Should not create duplicate allocation
512
512
  const allocations = await db.select().from(ipAllocations).all();
513
513
  expect(allocations).toHaveLength(1);
514
514
  });
515
515
 
516
- test('should skip IPAM allocation if vmid and container_ip already configured', async () => {
516
+ test('should skip IPAM allocation if vmid and target_ip already configured', async () => {
517
517
  // Create module with existing config
518
518
  db.insert(modules)
519
519
  .values({
@@ -528,7 +528,7 @@ describe('Variable Context', () => {
528
528
  variables: {
529
529
  owns: [
530
530
  { name: 'vmid', type: 'integer', required: true, source: 'user' },
531
- { name: 'container_ip', type: 'string', required: true, source: 'user' },
531
+ { name: 'target_ip', type: 'string', required: true, source: 'user' },
532
532
  ],
533
533
  },
534
534
  },
@@ -538,7 +538,7 @@ describe('Variable Context', () => {
538
538
  db.insert(moduleConfigs)
539
539
  .values([
540
540
  { moduleId: 'manual-config', key: 'vmid', value: '9999' },
541
- { moduleId: 'manual-config', key: 'container_ip', value: '192.168.99.99/24' },
541
+ { moduleId: 'manual-config', key: 'target_ip', value: '192.168.99.99/24' },
542
542
  ])
543
543
  .run();
544
544
 
@@ -546,15 +546,15 @@ describe('Variable Context', () => {
546
546
 
547
547
  // Should use existing config
548
548
  expect(context.selfConfig.vmid).toBe('9999');
549
- expect(context.selfConfig.container_ip).toBe('192.168.99.99/24');
549
+ expect(context.selfConfig.target_ip).toBe('192.168.99.99/24');
550
550
 
551
551
  // Should not create allocation
552
552
  const allocations = await db.select().from(ipAllocations).all();
553
553
  expect(allocations).toHaveLength(0);
554
554
  });
555
555
 
556
- test('should skip IPAM allocation for VPS modules without container_ip', async () => {
557
- // Create VPS module (no container_ip variable)
556
+ test('should skip IPAM allocation for VPS modules without target_ip', async () => {
557
+ // Create VPS module (no target_ip variable)
558
558
  db.insert(modules)
559
559
  .values({
560
560
  id: 'vps-module',
@@ -576,7 +576,7 @@ describe('Variable Context', () => {
576
576
 
577
577
  // Should not allocate IPAM resources
578
578
  expect(context.selfConfig.vmid).toBeUndefined();
579
- expect(context.selfConfig.container_ip).toBeUndefined();
579
+ expect(context.selfConfig.target_ip).toBeUndefined();
580
580
 
581
581
  const allocations = await db.select().from(ipAllocations).all();
582
582
  expect(allocations).toHaveLength(0);
@@ -597,7 +597,7 @@ describe('Variable Context', () => {
597
597
  variables: {
598
598
  owns: [
599
599
  { name: 'vmid', type: 'integer', required: true, source: 'user' },
600
- { name: 'container_ip', type: 'string', required: true, source: 'user' },
600
+ { name: 'target_ip', type: 'string', required: true, source: 'user' },
601
601
  ],
602
602
  },
603
603
  },
@@ -618,7 +618,7 @@ describe('Variable Context', () => {
618
618
  variables: {
619
619
  owns: [
620
620
  { name: 'vmid', type: 'integer', required: true, source: 'user' },
621
- { name: 'container_ip', type: 'string', required: true, source: 'user' },
621
+ { name: 'target_ip', type: 'string', required: true, source: 'user' },
622
622
  ],
623
623
  },
624
624
  },
@@ -634,8 +634,8 @@ describe('Variable Context', () => {
634
634
  expect(context2.selfConfig.vmid).toBe('2101');
635
635
 
636
636
  // Should have sequential IPs
637
- expect(context1.selfConfig.container_ip).toBe('10.0.10.10/24');
638
- expect(context2.selfConfig.container_ip).toBe('10.0.10.11/24');
637
+ expect(context1.selfConfig.target_ip).toBe('10.0.10.10/24');
638
+ expect(context2.selfConfig.target_ip).toBe('10.0.10.11/24');
639
639
 
640
640
  const allocations = await db.select().from(ipAllocations).all();
641
641
  expect(allocations).toHaveLength(2);
@@ -1254,7 +1254,7 @@ describe('Variable Context', () => {
1254
1254
 
1255
1255
  test('should auto-derive inventory variables in buildContextFromData', () => {
1256
1256
  const context = buildContextFromData('my-module', {
1257
- selfConfig: { container_ip: '10.0.30.15/24', hostname: 'my-host' },
1257
+ selfConfig: { target_ip: '10.0.30.15/24', hostname: 'my-host' },
1258
1258
  });
1259
1259
 
1260
1260
  expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.30.15');
@@ -92,7 +92,7 @@ async function autoAssignFromWellKnown(
92
92
  * Policy function - checks manifest and config
93
93
  *
94
94
  * A module needs IPAM if:
95
- * 1. It declares vmid and container_ip variables (container-based), AND
95
+ * 1. It declares vmid and target_ip variables (container-based), AND
96
96
  * 2. These values are not already set in module config
97
97
  *
98
98
  * @param manifest - Module manifest
@@ -106,16 +106,16 @@ function needsIpamAllocation(
106
106
  const variables = manifest.variables?.owns ?? [];
107
107
 
108
108
  const hasVmid = variables.some((v) => v.name === 'vmid');
109
- const hasContainerIp = variables.some((v) => v.name === 'container_ip');
109
+ const hasTargetIp = variables.some((v) => v.name === 'target_ip');
110
110
 
111
- // Module must declare both vmid and container_ip to be container-based
112
- if (!hasVmid || !hasContainerIp) {
111
+ // Module must declare both vmid and target_ip to be container-based
112
+ if (!hasVmid || !hasTargetIp) {
113
113
  return false;
114
114
  }
115
115
 
116
116
  // Check if already allocated (both must be present)
117
117
  const vmidSet = selfConfig.vmid !== undefined && selfConfig.vmid !== '';
118
- const ipSet = selfConfig.container_ip !== undefined && selfConfig.container_ip !== '';
118
+ const ipSet = selfConfig.target_ip !== undefined && selfConfig.target_ip !== '';
119
119
 
120
120
  // Need allocation if either is missing
121
121
  return !vmidSet || !ipSet;
@@ -162,7 +162,7 @@ function determineModuleZone(
162
162
  *
163
163
  * These variables are automatically available in Ansible templates:
164
164
  * - inventory.hostname: Derived from hostname variable
165
- * - inventory.ansible_host: Derived from container_ip (strips CIDR) or vps_ip
165
+ * - inventory.ansible_host: Derived from target_ip (strips CIDR) or vps_ip
166
166
  * - inventory.ansible_user: Defaults to "root"
167
167
  * - inventory.groups: Derived from module ID
168
168
  *
@@ -181,9 +181,9 @@ function autoDeriveInventoryVariables(
181
181
  derived['inventory.hostname'] = selfConfig.hostname;
182
182
  }
183
183
 
184
- // Auto-derive ansible_host from container_ip (strips CIDR) or vps_ip
185
- if (selfConfig.container_ip) {
186
- derived['inventory.ansible_host'] = stripCidr(selfConfig.container_ip);
184
+ // Auto-derive ansible_host from target_ip (strips CIDR) or vps_ip
185
+ if (selfConfig.target_ip) {
186
+ derived['inventory.ansible_host'] = stripCidr(selfConfig.target_ip);
187
187
  } else if (selfConfig.vps_ip) {
188
188
  // VPS-based modules use vps_ip directly (no CIDR to strip)
189
189
  derived['inventory.ansible_host'] = selfConfig.vps_ip;
@@ -329,7 +329,7 @@ export async function buildResolutionContext(
329
329
  }
330
330
 
331
331
  // IPAM auto-allocation
332
- // If module declares vmid/container_ip but they're not configured, allocate automatically
332
+ // If module declares vmid/target_ip but they're not configured, allocate automatically
333
333
  if (module?.manifestData) {
334
334
  const manifest = module.manifestData as ModuleManifest;
335
335
 
@@ -342,17 +342,17 @@ export async function buildResolutionContext(
342
342
  const existing = await getAllocation(moduleId, tx);
343
343
 
344
344
  let vmid: number;
345
- let containerIp: string;
345
+ let allocatedIp: string;
346
346
 
347
347
  if (existing) {
348
348
  // Use existing allocation
349
349
  vmid = existing.vmid;
350
- containerIp = existing.containerIp;
350
+ allocatedIp = existing.containerIp;
351
351
  } else {
352
352
  // Allocate new resources (persists to ipAllocations)
353
353
  const allocation = await allocateResources(moduleId, zone, tx);
354
354
  vmid = allocation.vmid;
355
- containerIp = allocation.containerIp;
355
+ allocatedIp = allocation.containerIp;
356
356
  }
357
357
 
358
358
  // Ensure values are in module config (upsert to handle existing keys)
@@ -366,15 +366,15 @@ export async function buildResolutionContext(
366
366
 
367
367
  await tx
368
368
  .insert(moduleConfigs)
369
- .values({ moduleId, key: 'container_ip', value: containerIp })
369
+ .values({ moduleId, key: 'target_ip', value: allocatedIp })
370
370
  .onConflictDoUpdate({
371
371
  target: [moduleConfigs.moduleId, moduleConfigs.key],
372
- set: { value: containerIp },
372
+ set: { value: allocatedIp },
373
373
  });
374
374
 
375
375
  // Update selfConfig with allocated values
376
376
  selfConfig.vmid = String(vmid);
377
- selfConfig.container_ip = containerIp;
377
+ selfConfig.target_ip = allocatedIp;
378
378
  });
379
379
  }
380
380
  }
@@ -3,14 +3,14 @@ import { hasVariables, isValidVariableFormat, parseVariables } from './parser';
3
3
 
4
4
  describe('parseVariables', () => {
5
5
  test('should parse self variables', () => {
6
- const content = 'ip: $self:container_ip';
6
+ const content = 'ip: $self:target_ip';
7
7
  const variables = parseVariables(content);
8
8
 
9
9
  expect(variables).toHaveLength(1);
10
10
  expect(variables[0]).toEqual({
11
11
  type: 'self',
12
- path: 'container_ip',
13
- raw: '$self:container_ip',
12
+ path: 'target_ip',
13
+ raw: '$self:target_ip',
14
14
  });
15
15
  });
16
16
 
@@ -52,7 +52,7 @@ describe('parseVariables', () => {
52
52
 
53
53
  test('should parse multiple variables in same content', () => {
54
54
  const content = `
55
- ip: $self:container_ip
55
+ ip: $self:target_ip
56
56
  dns: $capability:dns_external.nameserver
57
57
  key: $secret:api_key
58
58
  `;
@@ -101,7 +101,7 @@ resource "proxmox_lxc" "homebridge" {
101
101
  cores = $self:cores
102
102
  memory = $self:memory
103
103
  network {
104
- ip = "$self:container_ip/24"
104
+ ip = "$self:target_ip/24"
105
105
  gw = "$system:management.ip"
106
106
  }
107
107
  }
@@ -189,7 +189,7 @@ resource "proxmox_lxc" "homebridge" {
189
189
 
190
190
  describe('hasVariables', () => {
191
191
  test('should return true for content with variables', () => {
192
- expect(hasVariables('ip: $self:container_ip')).toBe(true);
192
+ expect(hasVariables('ip: $self:target_ip')).toBe(true);
193
193
  expect(hasVariables('$secret:key')).toBe(true);
194
194
  });
195
195
 
@@ -206,7 +206,7 @@ describe('hasVariables', () => {
206
206
 
207
207
  describe('isValidVariableFormat', () => {
208
208
  test('should validate correct variable formats', () => {
209
- expect(isValidVariableFormat('$self:container_ip')).toBe(true);
209
+ expect(isValidVariableFormat('$self:target_ip')).toBe(true);
210
210
  expect(isValidVariableFormat('$system:management.ip')).toBe(true);
211
211
  expect(isValidVariableFormat('$secret:api_key')).toBe(true);
212
212
  expect(isValidVariableFormat('$capability:dns_external.nameserver')).toBe(true);
@@ -219,7 +219,7 @@ describe('isValidVariableFormat', () => {
219
219
  });
220
220
 
221
221
  test('should reject invalid variable formats', () => {
222
- expect(isValidVariableFormat('self:container_ip')).toBe(false); // missing $
222
+ expect(isValidVariableFormat('self:target_ip')).toBe(false); // missing $
223
223
  expect(isValidVariableFormat('$unknown:value')).toBe(false); // invalid type
224
224
  expect(isValidVariableFormat('$self:')).toBe(false); // missing path
225
225
  expect(isValidVariableFormat('$self')).toBe(false); // missing colon and path
@@ -29,7 +29,7 @@ const mockDb = {
29
29
  const createContext = (overrides?: Partial<ResolutionContext>): ResolutionContext => ({
30
30
  moduleId: 'test-module',
31
31
  selfConfig: {
32
- container_ip: '192.168.0.50',
32
+ target_ip: '192.168.0.50',
33
33
  hostname: 'homebridge',
34
34
  cores: '2',
35
35
  'resources.machine.cpu': '2',
@@ -69,8 +69,8 @@ describe('resolveVariable', () => {
69
69
  test('should resolve self variable', async () => {
70
70
  const variable: VariableReference = {
71
71
  type: 'self',
72
- path: 'container_ip',
73
- raw: '$self:container_ip',
72
+ path: 'target_ip',
73
+ raw: '$self:target_ip',
74
74
  };
75
75
  const context = createContext();
76
76
 
@@ -245,7 +245,7 @@ describe('resolveVariable', () => {
245
245
 
246
246
  describe('resolveTemplate', () => {
247
247
  test('should resolve template with single variable', async () => {
248
- const template = 'ip: $self:container_ip';
248
+ const template = 'ip: $self:target_ip';
249
249
  const context = createContext();
250
250
 
251
251
  const result = await resolveTemplate(template, context, mockDb);
@@ -259,7 +259,7 @@ describe('resolveTemplate', () => {
259
259
  test('should resolve template with multiple variables', async () => {
260
260
  const template = `
261
261
  hostname: $self:hostname
262
- ip: $self:container_ip
262
+ ip: $self:target_ip
263
263
  domain: $system:network.domain
264
264
  `;
265
265
  const context = createContext();
@@ -276,7 +276,7 @@ domain: $system:network.domain
276
276
 
277
277
  test('should resolve template with mixed variable types', async () => {
278
278
  const template = `
279
- container_ip: $self:container_ip
279
+ target_ip: $self:target_ip
280
280
  management_ip: $system:management.ip
281
281
  dns_server: $capability:dns_external.nameserver
282
282
  api_key: $secret:api_key
@@ -287,7 +287,7 @@ api_key: $secret:api_key
287
287
 
288
288
  expect(result.success).toBe(true);
289
289
  if (result.success) {
290
- expect(result.content).toContain('container_ip: 192.168.0.50');
290
+ expect(result.content).toContain('target_ip: 192.168.0.50');
291
291
  expect(result.content).toContain('management_ip: 192.168.0.10');
292
292
  expect(result.content).toContain('dns_server: ns1.example.com');
293
293
  expect(result.content).toContain('api_key: secret123');
@@ -324,7 +324,7 @@ resource "proxmox_lxc" "container" {
324
324
  hostname = "$self:hostname"
325
325
  cores = $self:cores
326
326
  network {
327
- ip = "$self:container_ip/24"
327
+ ip = "$self:target_ip/24"
328
328
  gateway = "$system:management.ip"
329
329
  }
330
330
  environment = {
@@ -362,7 +362,7 @@ resource "proxmox_lxc" "container" {
362
362
 
363
363
  test('should return errors for missing variables', async () => {
364
364
  const template = `
365
- ip: $self:container_ip
365
+ ip: $self:target_ip
366
366
  missing: $self:missing_var
367
367
  another_missing: $secret:missing_secret
368
368
  `;
@@ -380,9 +380,9 @@ another_missing: $secret:missing_secret
380
380
 
381
381
  test('should handle repeated variables', async () => {
382
382
  const template = `
383
- ip1: $self:container_ip
384
- ip2: $self:container_ip
385
- ip3: $self:container_ip
383
+ ip1: $self:target_ip
384
+ ip2: $self:target_ip
385
+ ip3: $self:target_ip
386
386
  `;
387
387
  const context = createContext();
388
388
 
@@ -393,7 +393,7 @@ ip3: $self:container_ip
393
393
  expect(result.content).toContain('ip1: 192.168.0.50');
394
394
  expect(result.content).toContain('ip2: 192.168.0.50');
395
395
  expect(result.content).toContain('ip3: 192.168.0.50');
396
- expect(result.content).not.toContain('$self:container_ip');
396
+ expect(result.content).not.toContain('$self:target_ip');
397
397
  }
398
398
  });
399
399
 
@@ -420,7 +420,7 @@ $self:hostname
420
420
  const template = 'ansible_host: $self:inventory.ansible_host';
421
421
  const context = createContext({
422
422
  selfConfig: {
423
- container_ip: '10.0.10.10/24',
423
+ target_ip: '10.0.10.10/24',
424
424
  'inventory.ansible_host': '10.0.10.10', // Auto-derived (CIDR stripped)
425
425
  'inventory.ansible_user': 'root',
426
426
  'inventory.groups': 'test',
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * Variable reference parsed from template
7
- * Example: $self:container_ip -> { type: 'self', path: 'container_ip', raw: '$self:container_ip' }
7
+ * Example: $self:target_ip -> { type: 'self', path: 'target_ip', raw: '$self:target_ip' }
8
8
  */
9
9
  export interface VariableReference {
10
10
  type: 'self' | 'system' | 'system_secret' | 'secret' | 'capability';