iteration-cli 0.6.1 → 0.7.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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ npm install --global iteration-cli
7
7
  iteration connect
8
8
  ```
9
9
 
10
- The collectors currently run on macOS with Node.js 22 or later. `iteration connect` finds supported agents with local session directories, asks once before uploading their saved sessions and future activity, and connects them to one workspace. A connected collector checks for newly detected supported agents and connects them under that approval. Each source has its own background process, local checkpoint, and upload queue. Run `iteration` to see connected sources, `iteration status codex` or `iteration status claude` for details, and `iteration help` for other commands. `iteration connect codex` and `iteration connect claude` remain available for advanced source-specific setups.
10
+ The collectors run on macOS or Linux with Node.js 22 or later. Linux needs systemd. The CLI automatically enables background collection after logout and installs a user service that starts at boot and restarts on failure. If server policy does not let the user enable this, an administrator must allow background services for the account. The setting applies to the whole Linux account and remains enabled after `iteration disconnect`; the CLI does not turn off other user services. `iteration connect` finds supported agents with local session directories, asks once before uploading their saved sessions and future activity, and connects them to one workspace. A connected collector checks for newly detected supported agents and connects them under that approval. Each source has its own background process, local checkpoint, and upload queue. Run `iteration` to see connected sources, `iteration status codex` or `iteration status claude` for details, and `iteration help` for other commands. `iteration connect codex` and `iteration connect claude` remain available for advanced source-specific setups.
11
11
 
12
12
  Claude Code sessions are read from `~/.claude/projects`, including session subagents. If Claude uses `CLAUDE_CONFIG_DIR`, the collector follows that setting during connection; `iteration connect claude --claude-home DIR` selects another directory explicitly. The collector reads only saved local JSONL records. Claude Code may remove old transcripts under its own retention settings, so connect before local history is removed. The records can contain prompts, reasoning, tool input and output, and context snapshots; workspace members can read what uploads. Unknown record shapes are reported as capture gaps in the collector log. Remote Claude sessions are outside this local collector.
13
13
 
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import * as z from "zod";
6
6
  import { COLLECTOR_SOURCES, parseCollectorEnrollmentResponse, } from "iteration-trace-contracts";
7
- import { installLaunchAgent } from "./launch-agent.js";
7
+ import { installBackgroundService } from "./background-service.js";
8
8
  import { installTurnSummary } from "./turn-summary.js";
9
9
  import { assertFreshCollectorDirectory, collectorPaths, defaultCollectorDirectory, ensurePrivateDirectory, loadConfig, prepareSourceInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
10
10
  const consentSchema = z
@@ -159,7 +159,7 @@ export async function activateSource(source, home, endpoint, enrollment, state)
159
159
  await saveConfig(config, paths);
160
160
  try {
161
161
  await saveState(state, paths);
162
- await installLaunchAgent(directory, source);
162
+ await installBackgroundService(directory, source);
163
163
  }
164
164
  catch (error) {
165
165
  throw new Error(`${source} was connected, but local startup failed. Run iteration start ${source} to repair it.`, { cause: error });
@@ -0,0 +1,7 @@
1
+ import type { CollectorSource } from "iteration-trace-contracts";
2
+ export declare function requireCollectorPlatform(): void;
3
+ export declare function prepareBackgroundPersistence(): Promise<void>;
4
+ export declare function collectorLogPath(source: CollectorSource): string;
5
+ export declare function installBackgroundService(directory: string, source: CollectorSource): Promise<string>;
6
+ export declare function uninstallBackgroundService(source: CollectorSource): Promise<void>;
7
+ export declare function backgroundServiceStatus(source: CollectorSource): Promise<boolean>;
@@ -0,0 +1,37 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
4
+ import { installSystemdUserService, ensureSystemdUserLinger, systemdLogPath, systemdUserServiceStatus, uninstallSystemdUserService, } from "./systemd-user.js";
5
+ export function requireCollectorPlatform() {
6
+ if (process.platform !== "darwin" && process.platform !== "linux") {
7
+ throw new Error("Automatic startup is supported on macOS and Linux only.");
8
+ }
9
+ }
10
+ export async function prepareBackgroundPersistence() {
11
+ requireCollectorPlatform();
12
+ if (process.platform === "linux")
13
+ await ensureSystemdUserLinger();
14
+ }
15
+ export function collectorLogPath(source) {
16
+ if (process.platform === "linux")
17
+ return systemdLogPath(source);
18
+ return path.join(os.homedir(), "Library", "Logs", `iteration-${source}-collector.error.log`);
19
+ }
20
+ export async function installBackgroundService(directory, source) {
21
+ requireCollectorPlatform();
22
+ return process.platform === "darwin"
23
+ ? installLaunchAgent(directory, source)
24
+ : installSystemdUserService(directory, source);
25
+ }
26
+ export async function uninstallBackgroundService(source) {
27
+ requireCollectorPlatform();
28
+ return process.platform === "darwin"
29
+ ? uninstallLaunchAgent(source)
30
+ : uninstallSystemdUserService(source);
31
+ }
32
+ export async function backgroundServiceStatus(source) {
33
+ requireCollectorPlatform();
34
+ return process.platform === "darwin"
35
+ ? launchAgentStatus(source)
36
+ : systemdUserServiceStatus(source);
37
+ }
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { importSourceHistory } from "./history-import.js";
12
12
  import { runCollector } from "./service.js";
13
13
  import { renderDetailedStatus, renderHome, safeWorkspaceName, } from "./status-view.js";
14
14
  import { installTurnSummary, codexTurnSummaryApproval, uninstallTurnSummary, } from "./turn-summary.js";
15
- import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
15
+ import { backgroundServiceStatus, collectorLogPath, installBackgroundService, prepareBackgroundPersistence, requireCollectorPlatform, uninstallBackgroundService, } from "./background-service.js";
16
16
  import { assertFreshCollectorDirectory, collectorSource, collectorSourceDirectory, collectorPaths, defaultCollectorDirectory, ensureStartableState, healthSnapshot, historyScanProgress, loadConfig, outboxSize, pendingEventIds, prepareSourceInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
17
17
  const DEFAULT_INGEST_ENDPOINT = "https://useiteration.com/api/traces/ingest";
18
18
  function argumentsForCli() {
@@ -55,14 +55,14 @@ function usage(command) {
55
55
  if (command === "connect") {
56
56
  process.stdout.write([
57
57
  "Connect a coding agent to Iteration",
58
- " iteration connect Connect all detected agents on this Mac",
58
+ " iteration connect Connect all detected agents on this computer",
59
59
  " iteration connect codex",
60
60
  " iteration connect claude",
61
61
  "",
62
62
  "Existing local sessions and new activity are collected automatically.",
63
63
  "",
64
64
  "Options for advanced setups:",
65
- " --device-name NAME Name this Mac in Iteration",
65
+ " --device-name NAME Name this computer in Iteration",
66
66
  " --organization-id UUID Select a workspace without a prompt",
67
67
  " --codex-home DIR Use a different Codex data directory",
68
68
  " --claude-home DIR Use a different Claude data directory",
@@ -118,7 +118,7 @@ async function defaultCommand() {
118
118
  });
119
119
  const missing = discovered.filter((source) => (source === "codex" ? codex : claude).kind === "not_connected");
120
120
  if (missing.length > 0) {
121
- process.stdout.write(`\n${missing.map(collectorSourceLabel).join(" and ")} found on this Mac but not connected. Run iteration connect to include ${missing.length === 1 ? "it" : "them"}.\n`);
121
+ process.stdout.write(`\n${missing.map(collectorSourceLabel).join(" and ")} found on this computer but not connected. Run iteration connect to include ${missing.length === 1 ? "it" : "them"}.\n`);
122
122
  }
123
123
  return;
124
124
  }
@@ -145,9 +145,7 @@ async function turnSummaryNotice(config) {
145
145
  }
146
146
  }
147
147
  async function initialize(source, values) {
148
- if (process.platform !== "darwin") {
149
- throw new Error("Automatic startup is currently supported on macOS only.");
150
- }
148
+ requireCollectorPlatform();
151
149
  const endpoint = optionalString(values, "endpoint") ?? DEFAULT_INGEST_ENDPOINT;
152
150
  const endpointUrl = validateCollectorEndpoint(endpoint);
153
151
  const deviceName = optionalString(values, "device-name") ?? os.hostname();
@@ -163,6 +161,7 @@ async function initialize(source, values) {
163
161
  path.join(os.homedir(), ".claude")));
164
162
  await assertFreshCollectorDirectory(paths, source);
165
163
  const preparedState = await prepareSourceInitialState(source, sourceHome, true);
164
+ await prepareBackgroundPersistence();
166
165
  const enrollment = await authenticateAndEnrollCollector(endpointUrl, deviceName, organizationId, source);
167
166
  const connection = {
168
167
  endpoint: endpointUrl.toString(),
@@ -182,7 +181,7 @@ async function initialize(source, values) {
182
181
  throw new Error(`Collector access was approved, but local setup did not finish. Run iteration start ${source} to repair it.`, { cause: error });
183
182
  }
184
183
  try {
185
- await installLaunchAgent(directory, source);
184
+ await installBackgroundService(directory, source);
186
185
  }
187
186
  catch (error) {
188
187
  throw new Error(`Collector access was approved, but background startup failed. Run iteration start ${source} to retry it.`, { cause: error });
@@ -194,8 +193,8 @@ async function initialize(source, values) {
194
193
  async function approveAllSources(sources, workspaceName, yes) {
195
194
  const names = sources.map(collectorSourceLabel).join(" and ");
196
195
  const safeName = safeWorkspaceName(workspaceName);
197
- process.stdout.write(`Detected ${names || "no saved agent sessions"} on this Mac.\n` +
198
- `Connect supported agents${safeName ? ` to ${safeName}` : " to the workspace you select"}? Existing sessions and future activity, including prompts, code, and tool output, will be uploaded. Workspace members can read them. Newly detected supported agents will connect automatically.\n`);
196
+ process.stdout.write(`Detected ${names || "no saved agent sessions"} on this computer.\n` +
197
+ `Connect supported agents${safeName ? ` to ${safeName}` : " to the workspace you select"}? Existing sessions and future activity, including prompts, code, and tool output, will be uploaded. Workspace members can read them. Newly detected supported agents will connect automatically.${process.platform === "linux" ? " Collection will continue after you log out." : ""}\n`);
199
198
  if (yes)
200
199
  return;
201
200
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -217,9 +216,7 @@ async function approveAllSources(sources, workspaceName, yes) {
217
216
  }
218
217
  }
219
218
  async function connectAll(values) {
220
- if (process.platform !== "darwin") {
221
- throw new Error("Automatic startup is currently supported on macOS only.");
222
- }
219
+ requireCollectorPlatform();
223
220
  if (values["data-directory"] !== undefined) {
224
221
  throw new Error("--data-directory requires a source-specific connect command.");
225
222
  }
@@ -237,7 +234,7 @@ async function connectAll(values) {
237
234
  const detected = await detectedSources(sourceHomes);
238
235
  const existing = COLLECTOR_SOURCES.flatMap((source, index) => configs[index] ? [{ source, config: configs[index] }] : []);
239
236
  if (detected.length === 0 && existing.length === 0) {
240
- process.stdout.write("No supported local coding agents found yet. Run iteration connect after using Codex or Claude Code on this Mac.\n");
237
+ process.stdout.write("No supported local coding agents found yet. Run iteration connect after using Codex or Claude Code on this computer.\n");
241
238
  return;
242
239
  }
243
240
  const anchorSource = existing[0]?.source ?? detected[0];
@@ -275,6 +272,7 @@ async function connectAll(values) {
275
272
  previousConsent.sourceHomes.claude !== sourceHomes.claude) {
276
273
  await approveAllSources(detected, existingScopes[0]?.scope.organizationName ?? null, values.yes === true);
277
274
  }
275
+ await prepareBackgroundPersistence();
278
276
  let anchor = anchorExisting ?? (await initialize(anchorSource, values));
279
277
  let organizationId = requestedOrganizationId ??
280
278
  existingScopes[0]?.scope.organizationId ??
@@ -310,16 +308,14 @@ async function connectAll(values) {
310
308
  });
311
309
  for (const item of existing) {
312
310
  await ensureStartableState(item.config, collectorPaths(item.config.dataDirectory));
313
- await installLaunchAgent(item.config.dataDirectory, item.source);
311
+ await installBackgroundService(item.config.dataDirectory, item.source);
314
312
  const notice = await turnSummaryNotice(item.config);
315
313
  process.stdout.write(`${collectorSourceLabel(item.source)} is still connected.${notice}\n`);
316
314
  }
317
- process.stdout.write("Detected agents are connected. New supported agents on this Mac will connect automatically.\n");
315
+ process.stdout.write("Detected agents are connected. New supported agents on this computer will connect automatically.\n");
318
316
  }
319
317
  async function importHistory(source, values) {
320
- if (process.platform !== "darwin") {
321
- throw new Error("Automatic startup is currently supported on macOS only.");
322
- }
318
+ requireCollectorPlatform();
323
319
  const directory = dataDirectory(source, values);
324
320
  const paths = collectorPaths(directory);
325
321
  const config = await loadConfig(paths);
@@ -335,16 +331,14 @@ async function importHistory(source, values) {
335
331
  process.stdout.write(`History scan scheduled for ${result.size.files} local session files (${result.size.bytes} bytes). Scanning and uploads run in bounded batches and may take time. Older sessions appear under the date Iteration receives them. Use iteration status ${source} to see local scan progress and pending uploads.${result.backgroundRunning ? "" : ` The collector was not running; run iteration start ${source} to begin scanning.`}\n`);
336
332
  }
337
333
  async function start(source, values) {
338
- if (process.platform !== "darwin") {
339
- throw new Error("Automatic startup is currently supported on macOS only.");
340
- }
334
+ requireCollectorPlatform();
341
335
  const directory = dataDirectory(source, values);
342
336
  const paths = collectorPaths(directory);
343
337
  const config = await loadConfig(paths);
344
338
  if (collectorSource(config) !== source)
345
339
  throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
346
340
  const repairedState = await ensureStartableState(config, paths);
347
- await installLaunchAgent(directory, source);
341
+ await installBackgroundService(directory, source);
348
342
  const notice = await turnSummaryNotice(config);
349
343
  process.stdout.write(`${collectorSourceLabel(source)} is collecting again. Run iteration to check progress.${repairedState ? " Incomplete setup was repaired." : ""}${notice}\n`);
350
344
  }
@@ -364,7 +358,7 @@ async function statusSnapshot(source, values) {
364
358
  if (collectorSource(config) !== source)
365
359
  throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
366
360
  const [running, size, ids, scan, localHealth, approval] = await Promise.all([
367
- launchAgentStatus(source),
361
+ backgroundServiceStatus(source),
368
362
  outboxSize(paths),
369
363
  pendingEventIds(paths),
370
364
  historyScanProgress(collectorSourceDirectory(config), paths, source),
@@ -384,7 +378,7 @@ async function statusSnapshot(source, values) {
384
378
  healthAvailable: localHealth.kind === "available",
385
379
  turnSummaryNeedsApproval: approval === "needs_approval",
386
380
  tracesUrl: new URL("/app/sessions", config.endpoint).toString(),
387
- logPath: path.join(os.homedir(), "Library", "Logs", `iteration-${source}-collector.error.log`),
381
+ logPath: collectorLogPath(source),
388
382
  };
389
383
  }
390
384
  async function status(source, values) {
@@ -444,7 +438,7 @@ async function main() {
444
438
  if (command === "start" && selectedSource !== null)
445
439
  return start(selectedSource, values);
446
440
  if (command === "disconnect" && selectedSource !== null) {
447
- await uninstallLaunchAgent(selectedSource);
441
+ await uninstallBackgroundService(selectedSource);
448
442
  const paths = collectorPaths(dataDirectory(selectedSource, values));
449
443
  if (await fileExists(paths.config)) {
450
444
  await uninstallTurnSummary(await loadConfig(paths));
@@ -1,16 +1,16 @@
1
1
  import path from "node:path";
2
- import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
2
+ import { backgroundServiceStatus, installBackgroundService, uninstallBackgroundService, } from "./background-service.js";
3
3
  import { waitForCollectorLock } from "./service.js";
4
4
  import { collectorSource, collectorSourceDirectory, historyImportSize, loadState, resetHistoryCursors, } from "./storage.js";
5
- function macControl(source) {
5
+ function backgroundControl(source) {
6
6
  return {
7
- isRunning: () => launchAgentStatus(source),
8
- stop: () => uninstallLaunchAgent(source),
9
- start: (directory) => installLaunchAgent(directory, source),
7
+ isRunning: () => backgroundServiceStatus(source),
8
+ stop: () => uninstallBackgroundService(source),
9
+ start: (directory) => installBackgroundService(directory, source),
10
10
  acquireLock: waitForCollectorLock,
11
11
  };
12
12
  }
13
- export async function importSourceHistory(config, paths, confirmed, control = macControl(collectorSource(config))) {
13
+ export async function importSourceHistory(config, paths, confirmed, control = backgroundControl(collectorSource(config))) {
14
14
  const source = collectorSource(config);
15
15
  const directory = collectorSourceDirectory(config);
16
16
  const state = await loadState(paths);
@@ -44,7 +44,7 @@ function relativeTime(atMs, nowMs) {
44
44
  return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ago`;
45
45
  }
46
46
  function pendingDescription(status) {
47
- return `${status.pendingEvents} event${status.pendingEvents === 1 ? "" : "s"} (${readableBytes(status.pendingBytes)}) waiting on this Mac`;
47
+ return `${status.pendingEvents} event${status.pendingEvents === 1 ? "" : "s"} (${readableBytes(status.pendingBytes)}) waiting on this computer`;
48
48
  }
49
49
  function progressDescription(scan, source) {
50
50
  if (scan.kind === "waiting")
@@ -83,7 +83,7 @@ function failureDescription(status, source) {
83
83
  if (failure.status === 401 || failure.status === 403) {
84
84
  return {
85
85
  heading: `${name} upload access needs attention`,
86
- explanation: `Iteration denied this collector (${code}). Queued data stays on this Mac. Contact Iteration support to reconnect it safely.`,
86
+ explanation: `Iteration denied this collector (${code}). Queued data stays on this computer. Contact Iteration support to reconnect it safely.`,
87
87
  };
88
88
  }
89
89
  if (failure.status !== null &&
@@ -133,8 +133,8 @@ export function renderHome(status, nowMs = Date.now(), source = "codex") {
133
133
  `${name} connection needs repair`,
134
134
  "Local collector data exists, but its connection details are missing.",
135
135
  status.pendingBytes > 0
136
- ? `${readableBytes(status.pendingBytes)} of queued data remains on this Mac.`
137
- : "Existing local progress remains on this Mac.",
136
+ ? `${readableBytes(status.pendingBytes)} of queued data remains on this computer.`
137
+ : "Existing local progress remains on this computer.",
138
138
  `Run iteration status ${source} for recovery details.`,
139
139
  ].join("\n");
140
140
  }
@@ -184,7 +184,7 @@ export function renderHome(status, nowMs = Date.now(), source = "codex") {
184
184
  lines.push(`${name} is uploading captured sessions`, `${pendingDescription(status)}.`);
185
185
  }
186
186
  else if (status.scan.totalBytes === 0) {
187
- lines.push(`${name} is connected`, `Waiting for ${name} sessions on this Mac.`);
187
+ lines.push(`${name} is connected`, `Waiting for ${name} sessions on this computer.`);
188
188
  }
189
189
  else if (status.scan.kind === "scanning" &&
190
190
  status.scan.scannedBytes < status.scan.totalBytes) {
@@ -0,0 +1,7 @@
1
+ import type { CollectorSource } from "iteration-trace-contracts";
2
+ export declare function ensureSystemdUserLinger(): Promise<void>;
3
+ export declare function systemdLogPath(source: CollectorSource): string;
4
+ export declare function systemdUserUnit(cliPath?: string, nodePath?: string, dataDirectory?: string, source?: CollectorSource, outputPath?: string, errorPath?: string): string;
5
+ export declare function installSystemdUserService(dataDirectory: string, source: CollectorSource): Promise<string>;
6
+ export declare function uninstallSystemdUserService(source: CollectorSource): Promise<void>;
7
+ export declare function systemdUserServiceStatus(source: CollectorSource): Promise<boolean>;
@@ -0,0 +1,137 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ const execFile = promisify(execFileCallback);
8
+ const SYSTEMCTL_TIMEOUT_MS = 10_000;
9
+ const LOGINCTL_TIMEOUT_MS = 10_000;
10
+ function currentUserId() {
11
+ const userId = process.getuid?.();
12
+ if (userId === undefined)
13
+ throw new Error("Could not determine the current Linux user id.");
14
+ return String(userId);
15
+ }
16
+ async function lingerState(userId) {
17
+ const { stdout } = await execFile("loginctl", ["show-user", userId, "--property=Linger", "--value"], { timeout: LOGINCTL_TIMEOUT_MS });
18
+ const state = stdout.trim();
19
+ if (state === "yes" || state === "no")
20
+ return state;
21
+ throw new Error(`Could not read Linux background service setting: ${state || "empty response"}.`);
22
+ }
23
+ export async function ensureSystemdUserLinger() {
24
+ const userId = currentUserId();
25
+ let state = "no";
26
+ let queryFailure;
27
+ try {
28
+ state = await lingerState(userId);
29
+ }
30
+ catch (error) {
31
+ queryFailure = error;
32
+ }
33
+ if (state === "yes")
34
+ return;
35
+ try {
36
+ await execFile("loginctl", ["--no-ask-password", "enable-linger", userId], {
37
+ timeout: LOGINCTL_TIMEOUT_MS,
38
+ });
39
+ }
40
+ catch (userError) {
41
+ try {
42
+ await execFile("sudo", ["-n", "loginctl", "enable-linger", userId], {
43
+ timeout: LOGINCTL_TIMEOUT_MS,
44
+ });
45
+ }
46
+ catch (adminError) {
47
+ throw new Error("This server does not allow Iteration to keep collecting after you log out. Ask an administrator to allow background services for your account, then run iteration connect again.", {
48
+ cause: new AggregateError(queryFailure
49
+ ? [queryFailure, userError, adminError]
50
+ : [userError, adminError], "Could not enable Linux background collection."),
51
+ });
52
+ }
53
+ }
54
+ try {
55
+ if ((await lingerState(userId)) === "yes")
56
+ return;
57
+ }
58
+ catch (error) {
59
+ throw new Error("Could not verify Linux background collection after logout.", {
60
+ cause: error,
61
+ });
62
+ }
63
+ throw new Error("Linux did not enable background collection after logout.");
64
+ }
65
+ function unitName(source) {
66
+ return `iteration-${source}-collector.service`;
67
+ }
68
+ function unitPath(source) {
69
+ return path.join(os.homedir(), ".config", "systemd", "user", unitName(source));
70
+ }
71
+ export function systemdLogPath(source) {
72
+ return path.join(os.homedir(), ".config", "iteration", "logs", `iteration-${source}-collector.error.log`);
73
+ }
74
+ function unitValue(value) {
75
+ if (/[\r\n\0]/.test(value))
76
+ throw new Error("Collector paths cannot contain line breaks.");
77
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
78
+ }
79
+ export function systemdUserUnit(cliPath = fileURLToPath(new URL("./cli.js", import.meta.url)), nodePath = process.execPath, dataDirectory = path.join(os.homedir(), ".config", "iteration", "codex-collector"), source = "codex", outputPath = path.join(os.homedir(), ".config", "iteration", "logs", `iteration-${source}-collector.log`), errorPath = systemdLogPath(source)) {
80
+ return [
81
+ "[Unit]",
82
+ `Description=Iteration ${source} collector`,
83
+ "[Service]",
84
+ "Type=exec",
85
+ `ExecStart=${[nodePath, cliPath, "run", source, "--data-directory", dataDirectory].map(unitValue).join(" ")}`,
86
+ "Restart=always",
87
+ "RestartSec=5",
88
+ `StandardOutput=append:${unitValue(outputPath)}`,
89
+ `StandardError=append:${unitValue(errorPath)}`,
90
+ "[Install]",
91
+ "WantedBy=default.target",
92
+ "",
93
+ ].join("\n");
94
+ }
95
+ async function systemctl(...args) {
96
+ const { stdout } = await execFile("systemctl", ["--user", ...args], {
97
+ timeout: SYSTEMCTL_TIMEOUT_MS,
98
+ });
99
+ return stdout.trim();
100
+ }
101
+ export async function installSystemdUserService(dataDirectory, source) {
102
+ await ensureSystemdUserLinger();
103
+ const file = unitPath(source);
104
+ const logDirectory = path.dirname(systemdLogPath(source));
105
+ await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
106
+ await fs.mkdir(logDirectory, { recursive: true, mode: 0o700 });
107
+ await fs.writeFile(file, systemdUserUnit(undefined, undefined, dataDirectory, source), {
108
+ encoding: "utf8",
109
+ mode: 0o644,
110
+ });
111
+ await systemctl("daemon-reload");
112
+ await systemctl("enable", unitName(source));
113
+ await systemctl("restart", unitName(source));
114
+ return file;
115
+ }
116
+ export async function uninstallSystemdUserService(source) {
117
+ const file = unitPath(source);
118
+ try {
119
+ await fs.access(file);
120
+ }
121
+ catch (error) {
122
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
123
+ return;
124
+ throw error;
125
+ }
126
+ await systemctl("disable", "--now", unitName(source));
127
+ await fs.rm(file);
128
+ await systemctl("daemon-reload");
129
+ }
130
+ export async function systemdUserServiceStatus(source) {
131
+ const state = await systemctl("show", "--property=ActiveState", "--value", unitName(source));
132
+ if (state === "active")
133
+ return true;
134
+ if (state === "inactive" || state === "failed")
135
+ return false;
136
+ throw new Error(`Could not determine ${source} collector service state: ${state || "empty response"}.`);
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iteration-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",