@love-moon/conductor-cli 0.5.1 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @love-moon/conductor-cli
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bcc80b5: Initialize Git submodules automatically when preparing task worktrees.
8
+
9
+ ### Patch Changes
10
+
11
+ - @love-moon/conductor-sdk@0.6.0
12
+ - @love-moon/ai-sdk@0.6.0
13
+
3
14
  ## 0.5.1
4
15
 
5
16
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.5.1",
4
- "gitCommitId": "119eab6",
3
+ "version": "0.6.0",
4
+ "gitCommitId": "2f914a7",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -23,8 +23,8 @@
23
23
  "test": "node --test test/*.test.js"
24
24
  },
25
25
  "dependencies": {
26
- "@love-moon/ai-sdk": "0.5.1",
27
- "@love-moon/conductor-sdk": "0.5.1",
26
+ "@love-moon/ai-sdk": "0.6.0",
27
+ "@love-moon/conductor-sdk": "0.6.0",
28
28
  "@github/copilot-sdk": "^0.3.0",
29
29
  "chrome-launcher": "^1.2.1",
30
30
  "chrome-remote-interface": "^0.33.0",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "optionalDependencies": {
39
39
  "@roamhq/wrtc": "^0.10.0",
40
- "@love-moon/chat-web": "0.5.1"
40
+ "@love-moon/chat-web": "0.6.0"
41
41
  },
42
42
  "pnpm": {
43
43
  "onlyBuiltDependencies": [
package/src/daemon.js CHANGED
@@ -1256,6 +1256,8 @@ export function startDaemon(config = {}, deps = {}) {
1256
1256
  "",
1257
1257
  "worktree:",
1258
1258
  " sync_branch: false",
1259
+ " sync_submodules: true",
1260
+ " # When .gitmodules exists, initialize/update submodules in task worktrees.",
1259
1261
  " symlink: []",
1260
1262
  " # Example: symlink paths from the parent workspace into each worktree",
1261
1263
  " # symlink:",
@@ -1264,6 +1266,72 @@ export function startDaemon(config = {}, deps = {}) {
1264
1266
  "",
1265
1267
  ].join("\n");
1266
1268
 
1269
+ const MAX_PROJECT_ICON_IMAGE_BYTES = 128 * 1024;
1270
+ const PROJECT_ICON_MIME_BY_EXTENSION = {
1271
+ ".svg": "image/svg+xml",
1272
+ ".png": "image/png",
1273
+ ".jpg": "image/jpeg",
1274
+ ".jpeg": "image/jpeg",
1275
+ ".gif": "image/gif",
1276
+ ".webp": "image/webp",
1277
+ ".ico": "image/x-icon",
1278
+ ".avif": "image/avif",
1279
+ };
1280
+
1281
+ function getProjectSettingsCandidates(projectWorkspacePath) {
1282
+ return [
1283
+ path.join(projectWorkspacePath, ".conductor", "settings.yaml"),
1284
+ path.join(projectWorkspacePath, ".conductor", "settings.yml"),
1285
+ path.join(projectWorkspacePath, ".conductor", "setttings.yaml"),
1286
+ path.join(projectWorkspacePath, ".conductor", "setttings.yml"),
1287
+ ];
1288
+ }
1289
+
1290
+ function extractProjectIconFromSettings(parsed) {
1291
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1292
+ return null;
1293
+ }
1294
+ const direct = normalizeOptionalString(parsed.icon);
1295
+ if (direct) return direct;
1296
+ const projectSettings = parsed.project;
1297
+ if (
1298
+ projectSettings &&
1299
+ typeof projectSettings === "object" &&
1300
+ !Array.isArray(projectSettings)
1301
+ ) {
1302
+ return normalizeOptionalString(projectSettings.icon);
1303
+ }
1304
+ return null;
1305
+ }
1306
+
1307
+ function projectIconLooksLikeFilesystemPath(iconValue) {
1308
+ if (/^(https?:\/\/|data:)/i.test(iconValue)) return false;
1309
+ if (iconValue.startsWith("/") || iconValue.startsWith("./") || iconValue.startsWith("../")) {
1310
+ return true;
1311
+ }
1312
+ if (!iconValue.includes("/")) return false;
1313
+ const ext = path.extname(iconValue).toLowerCase();
1314
+ return Object.prototype.hasOwnProperty.call(PROJECT_ICON_MIME_BY_EXTENSION, ext);
1315
+ }
1316
+
1317
+ function readProjectIconAsDataUri(settingsDir, iconValue) {
1318
+ const resolvedPath = path.isAbsolute(iconValue)
1319
+ ? iconValue
1320
+ : path.resolve(settingsDir, iconValue);
1321
+ const ext = path.extname(resolvedPath).toLowerCase();
1322
+ const mime = PROJECT_ICON_MIME_BY_EXTENSION[ext];
1323
+ if (!mime) return null;
1324
+ try {
1325
+ const stat = statSyncFn(resolvedPath);
1326
+ if (!stat.isFile() || stat.size === 0 || stat.size > MAX_PROJECT_ICON_IMAGE_BYTES) {
1327
+ return null;
1328
+ }
1329
+ return `data:${mime};base64,${readFileSyncFn(resolvedPath).toString("base64")}`;
1330
+ } catch {
1331
+ return null;
1332
+ }
1333
+ }
1334
+
1267
1335
  function ensureProjectSettingsTemplate(projectWorkspacePath) {
1268
1336
  const settingsPath = path.join(projectWorkspacePath, ".conductor", "settings.yaml");
1269
1337
  if (existsSyncFn(settingsPath)) {
@@ -1277,15 +1345,28 @@ export function startDaemon(config = {}, deps = {}) {
1277
1345
  }
1278
1346
  }
1279
1347
 
1280
- function readProjectWorktreeSettings(projectWorkspacePath) {
1281
- const settingsCandidates = [
1282
- path.join(projectWorkspacePath, ".conductor", "settings.yaml"),
1283
- path.join(projectWorkspacePath, ".conductor", "settings.yml"),
1284
- path.join(projectWorkspacePath, ".conductor", "setttings.yaml"),
1285
- path.join(projectWorkspacePath, ".conductor", "setttings.yml"),
1286
- ];
1348
+ function readProjectIconSetting(projectWorkspacePath) {
1349
+ for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
1350
+ if (!existsSyncFn(settingsPath)) {
1351
+ continue;
1352
+ }
1353
+ let rawIcon = null;
1354
+ try {
1355
+ rawIcon = extractProjectIconFromSettings(yaml.load(readFileSyncFn(settingsPath, "utf8")));
1356
+ } catch {
1357
+ return null;
1358
+ }
1359
+ if (!rawIcon) return null;
1360
+ if (!projectIconLooksLikeFilesystemPath(rawIcon)) {
1361
+ return rawIcon;
1362
+ }
1363
+ return readProjectIconAsDataUri(path.dirname(settingsPath), rawIcon);
1364
+ }
1365
+ return null;
1366
+ }
1287
1367
 
1288
- for (const settingsPath of settingsCandidates) {
1368
+ function readProjectWorktreeSettings(projectWorkspacePath) {
1369
+ for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
1289
1370
  if (!existsSyncFn(settingsPath)) {
1290
1371
  continue;
1291
1372
  }
@@ -1299,6 +1380,7 @@ export function startDaemon(config = {}, deps = {}) {
1299
1380
  return {
1300
1381
  symlinkPaths: normalizeConfiguredPathList(worktreeSettings.symlink, projectWorkspacePath),
1301
1382
  syncBranch: worktreeSettings.sync_branch === true || worktreeSettings.syncBranch === true,
1383
+ syncSubmodules: worktreeSettings.sync_submodules !== false && worktreeSettings.syncSubmodules !== false,
1302
1384
  settingsPath,
1303
1385
  };
1304
1386
  } catch (error) {
@@ -1309,6 +1391,7 @@ export function startDaemon(config = {}, deps = {}) {
1309
1391
  return {
1310
1392
  symlinkPaths: [],
1311
1393
  syncBranch: false,
1394
+ syncSubmodules: true,
1312
1395
  settingsPath: null,
1313
1396
  };
1314
1397
  }
@@ -1378,6 +1461,28 @@ export function startDaemon(config = {}, deps = {}) {
1378
1461
  }
1379
1462
  }
1380
1463
 
1464
+ async function ensureTaskWorktreeSubmodules({ taskId, projectWorkspacePath, worktreeRoot }) {
1465
+ const { syncSubmodules } = readProjectWorktreeSettings(projectWorkspacePath);
1466
+ if (!syncSubmodules || !existsSyncFn(path.join(worktreeRoot, ".gitmodules"))) {
1467
+ return;
1468
+ }
1469
+
1470
+ try {
1471
+ await runSpawnProcess(
1472
+ "git",
1473
+ ["-C", worktreeRoot, "submodule", "sync", "--recursive"],
1474
+ { cwd: worktreeRoot, timeoutMs: WORKTREE_SUBMODULE_SYNC_TIMEOUT_MS },
1475
+ );
1476
+ await runSpawnProcess(
1477
+ "git",
1478
+ ["-C", worktreeRoot, "submodule", "update", "--init", "--recursive"],
1479
+ { cwd: worktreeRoot, timeoutMs: WORKTREE_SUBMODULE_SYNC_TIMEOUT_MS },
1480
+ );
1481
+ } catch (error) {
1482
+ throw new Error(`Failed to sync git submodules for ${taskId}: ${error?.message || error}`);
1483
+ }
1484
+ }
1485
+
1381
1486
  async function runSpawnProcess(command, args, options = {}) {
1382
1487
  let child;
1383
1488
  const { timeoutMs, ...spawnOptions } = options || {};
@@ -1566,6 +1671,11 @@ export function startDaemon(config = {}, deps = {}) {
1566
1671
  }
1567
1672
  }
1568
1673
 
1674
+ await ensureTaskWorktreeSubmodules({
1675
+ taskId,
1676
+ projectWorkspacePath: worktreeConfig.projectWorkspacePath,
1677
+ worktreeRoot,
1678
+ });
1569
1679
  mkdirSyncFn(finalCwd, { recursive: true });
1570
1680
  await ensureTaskWorktreeSymlinks({
1571
1681
  projectRepoRoot: worktreeConfig.projectRepoRoot,
@@ -1601,6 +1711,10 @@ export function startDaemon(config = {}, deps = {}) {
1601
1711
  process.env.CONDUCTOR_WORKTREE_SYNC_TIMEOUT_MS,
1602
1712
  5_000,
1603
1713
  );
1714
+ const WORKTREE_SUBMODULE_SYNC_TIMEOUT_MS = parsePositiveInt(
1715
+ process.env.CONDUCTOR_WORKTREE_SUBMODULE_SYNC_TIMEOUT_MS,
1716
+ 120_000,
1717
+ );
1604
1718
  const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
1605
1719
  process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
1606
1720
  1000,
@@ -4338,6 +4452,7 @@ export function startDaemon(config = {}, deps = {}) {
4338
4452
  lastCommit: null,
4339
4453
  lastCommitAt: null,
4340
4454
  fileCount: null,
4455
+ icon: null,
4341
4456
  error: null,
4342
4457
  errorCode: null,
4343
4458
  validatedAt,
@@ -4391,6 +4506,7 @@ export function startDaemon(config = {}, deps = {}) {
4391
4506
  typeof snapshot?.fileCount === "number" && Number.isInteger(snapshot.fileCount)
4392
4507
  ? snapshot.fileCount
4393
4508
  : null,
4509
+ icon: readProjectIconSetting(effectiveWorkspace),
4394
4510
  };
4395
4511
  }
4396
4512
  } catch (error) {
@@ -4414,6 +4530,7 @@ export function startDaemon(config = {}, deps = {}) {
4414
4530
  last_commit_at: result.lastCommitAt,
4415
4531
  git_remote_url: result.gitRemoteUrl,
4416
4532
  file_count: result.fileCount,
4533
+ icon: result.icon,
4417
4534
  error: result.error,
4418
4535
  error_code: result.errorCode,
4419
4536
  validated_at: result.validatedAt,