@serviceme/devtools-core 0.2.0 → 0.2.1

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/dist/index.d.ts CHANGED
@@ -1318,6 +1318,12 @@ declare class RepoManager {
1318
1318
  hasLocalClone(repoId: string): Promise<boolean>;
1319
1319
  /** Force-create the SERVICEME home directory tree (idempotent). */
1320
1320
  ensureHome(): Promise<void>;
1321
+ /**
1322
+ * Returns `true` when `p` contains a `.git` entry — i.e. it is an
1323
+ * initialised git repository and safe to `pull`. Empty directories
1324
+ * (e.g. from an interrupted clone) return `false`.
1325
+ */
1326
+ private isValidGitRepo;
1321
1327
  private pathExists;
1322
1328
  }
1323
1329
 
@@ -1654,14 +1660,12 @@ interface MigrationResult {
1654
1660
  /** Soft issues surfaced by migrateV1ToV2 (e.g. unknown taskType). */
1655
1661
  issues: string[];
1656
1662
  }
1657
- declare class MigrateToGlobal {
1658
- /**
1659
- * Run the migration. Idempotent: each v1 file is deleted after processing,
1660
- * so re-running this method on a partially-migrated state is safe (the
1661
- * already-migrated files no longer exist; only the remainder is processed).
1662
- */
1663
- static run(options: MigrateToGlobalOptions): Promise<MigrationResult>;
1664
- }
1663
+ /**
1664
+ * Run the migration. Idempotent: each v1 file is deleted after processing,
1665
+ * so re-running this on a partially-migrated state is safe (the
1666
+ * already-migrated files no longer exist; only the remainder is processed).
1667
+ */
1668
+ declare function migrateToGlobal(options: MigrateToGlobalOptions): Promise<MigrationResult>;
1665
1669
 
1666
1670
  interface TaskEventListener {
1667
1671
  onStarted(params: TaskStartedEventParams): void;
@@ -1816,4 +1820,4 @@ declare class SkillStore {
1816
1820
  declare const unzipFile: (zipPath: string, dest: string) => Promise<void>;
1817
1821
  declare const moveFiles: (sourceDir: string, destDir: string, overwrite?: boolean) => Promise<void>;
1818
1822
 
1819
- export { AGENT_DRAFTS_SUBDIR, type AddUserRepoInput, type AddUserRepoResult, type AgentCatalog, AgentCatalogClient, type AgentCatalogClientOptions, type AgentDownloadFile, type AgentInstallScope, type AgentMutateResult, AgentReconciler, type AgentReconcilerDependencies, AgentStore, type AgentStoreFileSystem, type AgentStoreOptions, type AgentsStateFile, type AnyRepoConfig, type AppendLogInput, type BootstrapPhase5Result, CACHE_SUBDIR, CREDENTIALS_CONFIG_FILENAME, CannotRemoveDefaultRepoError, type CreateTaskInput, FIXED_ADDED_AT as DEFAULT_REPO_FIXED_ADDED_AT, DEFAULT_REPO_ID, DEVICE_JSON_FILENAME, DRAFTS_SUBDIR, DaemonLogger, type DefaultRepoConfig, type DraftDetail, DraftNotFoundError, type DraftSummary, DraftsError, DraftsStore, type EditTaskInput, EnvironmentInspector, type EnvironmentInspectorOptions, type ExecutorResult, type FsWatcherCallbacks, type FsWatcherHandle, GitClient, GithubCopilotCliExecutor, HttpRequestExecutor, ImageTools, type InstalledAgent, InvalidDraftError, InvalidRepoUrlError, type IsoTimestamp, type JsonTools, KNOWN_WORKSPACES_FILENAME, type LoadResult, MACHINE_ID_FILENAME, MIGRATION_FAILURES_FILENAME, MigrateToGlobal, type MigrateToGlobalOptions, type MigrationResult, type OutputEventCallback, PROFILES_JSON_FILENAME, PidManager, type ProbeError, type ProbeOptions, type ProbeResult, ProjectTools, REPOS_CONFIG_FILENAME, REPOS_SUBDIR, RepoAlreadyExistsError, RepoCloneConflictError, type RepoConfig, RepoManager, RepoManagerError, type RepoManagerOptions, RepoNotFoundError, RepoNotInStoreError, type RepoSource, type ReposFile, type ReposFileInput, ReposLoader, type ReposLoaderFileSystem, type ReposLoaderOptions, ReposStore, type ReposStoreChange, type ReposStoreFileSystem, type ReposStoreListener, type ReposStoreOptions, SAFE_REPO_ID_PATTERN, SCHEDULED_TASKS_CONFIG_FILENAME, SCHEDULED_TASKS_LOG_FILENAME, SCHEDULER_LOCK_FILENAME, SCHEDULER_LOG_FILENAME, SCHEDULER_PID_FILENAME, SERVICEME_DIR_NAME, SERVICEME_HOME_ENV, SKILL_DRAFTS_SUBDIR, type SaveDraftOptions, SchedulerDaemon, SchedulerDaemonV2, type SchedulerDaemonV2Options, type ServiceMeLogger, ShellExecutor, type SkillCatalog, SkillCatalogClient, type SkillCatalogClientOptions, type SkillDownloadFile, type SkillMutateResult, SkillReconciler, type SkillReconcilerDependencies, SkillStore, type SkillStoreFileSystem, type SkillStoreOptions, type StreamingExecutorHandle, type StreamingTaskExecutor, type SyncReport, type SyncStatus, TOOLBOX_JSON_FILENAME, TOOL_RISK_MAP, TaskConfigManager, type TaskEventListener, TaskExecutionEngine, type TaskExecutor, TaskLogManager, type UserRepoConfig, WorkspaceProbe, repoSchema as anyRepoConfigSchema, assertSafeRepoId, bootstrapDefaults, bootstrapPhase5Placeholders, buildDefaultReposFile, copilotDoctor, copilotPrompt, createConsoleLogger, createCopilotAuthRequiredError, createCopilotNotInstalledError, createImageTools, createJsonTools, createProjectTools, createReposStore, defaultRepoSchema as defaultRepoConfigSchema, ensureDefaultsInstalled, generateDraftId, getAgentDraftsDir, getAllDefaultRepoConfigs, getCacheDir, getCredentialsConfigPath, getDefaultRepoConfig, getDeviceJsonPath, getDraftsDir, getExecutor, getHomeDir, getHomePlatform, getKnownWorkspacesPath, getMachineIdPath, getMigrationFailuresPath, getProfilesJsonPath, getRepoCacheDir, getRepoDir, getReposConfigPath, getReposDir, getScheduledTasksConfigPath, getScheduledTasksLogPath, getSchedulerLockPath, getSchedulerLogPath, getSchedulerPidPath, getServicemeHome, getSkillDraftsDir, getToolboxJsonPath, isCopilotAuthenticated, isDefaultRepo, isStreamingTaskExecutor, isUserRepo, matchesCron, moveFiles, narrowRepoConfig, noopLogger, parseAgentToolPermissions, parseIntervalMs, reposFileSchema, resetUserHomeOverrides, resolveDraftDir, resolveTaskExecutionPayload, setUserHomeOverrides, unzipFile, userRepoSchema as userRepoConfigSchema, validateReposFile, validateTaskPayload };
1823
+ export { AGENT_DRAFTS_SUBDIR, type AddUserRepoInput, type AddUserRepoResult, type AgentCatalog, AgentCatalogClient, type AgentCatalogClientOptions, type AgentDownloadFile, type AgentInstallScope, type AgentMutateResult, AgentReconciler, type AgentReconcilerDependencies, AgentStore, type AgentStoreFileSystem, type AgentStoreOptions, type AgentsStateFile, type AnyRepoConfig, type AppendLogInput, type BootstrapPhase5Result, CACHE_SUBDIR, CREDENTIALS_CONFIG_FILENAME, CannotRemoveDefaultRepoError, type CreateTaskInput, FIXED_ADDED_AT as DEFAULT_REPO_FIXED_ADDED_AT, DEFAULT_REPO_ID, DEVICE_JSON_FILENAME, DRAFTS_SUBDIR, DaemonLogger, type DefaultRepoConfig, type DraftDetail, DraftNotFoundError, type DraftSummary, DraftsError, DraftsStore, type EditTaskInput, EnvironmentInspector, type EnvironmentInspectorOptions, type ExecutorResult, type FsWatcherCallbacks, type FsWatcherHandle, GitClient, GithubCopilotCliExecutor, HttpRequestExecutor, ImageTools, type InstalledAgent, InvalidDraftError, InvalidRepoUrlError, type IsoTimestamp, type JsonTools, KNOWN_WORKSPACES_FILENAME, type LoadResult, MACHINE_ID_FILENAME, MIGRATION_FAILURES_FILENAME, type MigrateToGlobalOptions, type MigrationResult, type OutputEventCallback, PROFILES_JSON_FILENAME, PidManager, type ProbeError, type ProbeOptions, type ProbeResult, ProjectTools, REPOS_CONFIG_FILENAME, REPOS_SUBDIR, RepoAlreadyExistsError, RepoCloneConflictError, type RepoConfig, RepoManager, RepoManagerError, type RepoManagerOptions, RepoNotFoundError, RepoNotInStoreError, type RepoSource, type ReposFile, type ReposFileInput, ReposLoader, type ReposLoaderFileSystem, type ReposLoaderOptions, ReposStore, type ReposStoreChange, type ReposStoreFileSystem, type ReposStoreListener, type ReposStoreOptions, SAFE_REPO_ID_PATTERN, SCHEDULED_TASKS_CONFIG_FILENAME, SCHEDULED_TASKS_LOG_FILENAME, SCHEDULER_LOCK_FILENAME, SCHEDULER_LOG_FILENAME, SCHEDULER_PID_FILENAME, SERVICEME_DIR_NAME, SERVICEME_HOME_ENV, SKILL_DRAFTS_SUBDIR, type SaveDraftOptions, SchedulerDaemon, SchedulerDaemonV2, type SchedulerDaemonV2Options, type ServiceMeLogger, ShellExecutor, type SkillCatalog, SkillCatalogClient, type SkillCatalogClientOptions, type SkillDownloadFile, type SkillMutateResult, SkillReconciler, type SkillReconcilerDependencies, SkillStore, type SkillStoreFileSystem, type SkillStoreOptions, type StreamingExecutorHandle, type StreamingTaskExecutor, type SyncReport, type SyncStatus, TOOLBOX_JSON_FILENAME, TOOL_RISK_MAP, TaskConfigManager, type TaskEventListener, TaskExecutionEngine, type TaskExecutor, TaskLogManager, type UserRepoConfig, WorkspaceProbe, repoSchema as anyRepoConfigSchema, assertSafeRepoId, bootstrapDefaults, bootstrapPhase5Placeholders, buildDefaultReposFile, copilotDoctor, copilotPrompt, createConsoleLogger, createCopilotAuthRequiredError, createCopilotNotInstalledError, createImageTools, createJsonTools, createProjectTools, createReposStore, defaultRepoSchema as defaultRepoConfigSchema, ensureDefaultsInstalled, generateDraftId, getAgentDraftsDir, getAllDefaultRepoConfigs, getCacheDir, getCredentialsConfigPath, getDefaultRepoConfig, getDeviceJsonPath, getDraftsDir, getExecutor, getHomeDir, getHomePlatform, getKnownWorkspacesPath, getMachineIdPath, getMigrationFailuresPath, getProfilesJsonPath, getRepoCacheDir, getRepoDir, getReposConfigPath, getReposDir, getScheduledTasksConfigPath, getScheduledTasksLogPath, getSchedulerLockPath, getSchedulerLogPath, getSchedulerPidPath, getServicemeHome, getSkillDraftsDir, getToolboxJsonPath, isCopilotAuthenticated, isDefaultRepo, isStreamingTaskExecutor, isUserRepo, matchesCron, migrateToGlobal, moveFiles, narrowRepoConfig, noopLogger, parseAgentToolPermissions, parseIntervalMs, reposFileSchema, resetUserHomeOverrides, resolveDraftDir, resolveTaskExecutionPayload, setUserHomeOverrides, unzipFile, userRepoSchema as userRepoConfigSchema, validateReposFile, validateTaskPayload };
package/dist/index.js CHANGED
@@ -75,7 +75,6 @@ __export(src_exports, {
75
75
  MIGRATION_FAILURES_FILENAME: () => MIGRATION_FAILURES_FILENAME,
76
76
  MicrosoftAuthProvider: () => MicrosoftAuthProvider,
77
77
  MicrosoftProviderNotHostedError: () => MicrosoftProviderNotHostedError,
78
- MigrateToGlobal: () => MigrateToGlobal,
79
78
  NodeGitSpawner: () => NodeGitSpawner,
80
79
  PROFILES_JSON_FILENAME: () => PROFILES_JSON_FILENAME,
81
80
  PidManager: () => PidManager,
@@ -174,6 +173,7 @@ __export(src_exports, {
174
173
  isUserRepo: () => isUserRepo,
175
174
  matchesCron: () => matchesCron,
176
175
  mergeWithDefaults: () => mergeWithDefaults,
176
+ migrateToGlobal: () => migrateToGlobal,
177
177
  moveFiles: () => moveFiles,
178
178
  narrowRepoConfig: () => narrowRepoConfig,
179
179
  noopLogger: () => noopLogger,
@@ -1035,6 +1035,11 @@ var DEFAULT_DEVICE_CODE_URL = "https://github.com/login/device/code";
1035
1035
  var DEFAULT_TOKEN_URL = "https://github.com/login/oauth/access_token";
1036
1036
  var DEFAULT_USER_URL = "https://api.github.com/user";
1037
1037
  var DEFAULT_SCOPE = "read:user user:email";
1038
+ var DEVICE_CODE_MAX_ATTEMPTS = 3;
1039
+ var DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS = 500;
1040
+ function isTransientNetworkError(error) {
1041
+ return error instanceof TypeError;
1042
+ }
1038
1043
  var GitHubAuthProvider = class {
1039
1044
  constructor(config) {
1040
1045
  this.providerId = "github";
@@ -1050,7 +1055,8 @@ var GitHubAuthProvider = class {
1050
1055
  minPollIntervalMs: config.minPollIntervalMs ?? 1e3,
1051
1056
  maxPollIntervalMs: config.maxPollIntervalMs ?? 15e3,
1052
1057
  maxWaitMs: config.maxWaitMs,
1053
- fetchImpl: config.fetchImpl ?? fetch
1058
+ fetchImpl: config.fetchImpl ?? fetch,
1059
+ deviceCodeRetryBaseDelayMs: config.deviceCodeRetryBaseDelayMs ?? DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS
1054
1060
  };
1055
1061
  }
1056
1062
  async requestDeviceFlow(opts) {
@@ -1058,29 +1064,45 @@ var GitHubAuthProvider = class {
1058
1064
  client_id: this.cfg.clientId,
1059
1065
  scope: opts?.scope ?? this.cfg.scope
1060
1066
  });
1061
- const resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
1062
- method: "POST",
1063
- headers: {
1064
- Accept: "application/json",
1065
- "Content-Type": "application/json",
1066
- "User-Agent": "serviceme-core"
1067
- },
1068
- body
1069
- });
1070
- if (!resp.ok) {
1071
- throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
1072
- }
1073
- const data = await resp.json();
1074
- if (!data.device_code || !data.user_code || !data.verification_uri) {
1075
- throw new Error("GitHub device-code response missing required fields");
1067
+ let lastNetworkError;
1068
+ for (let attempt = 1; attempt <= DEVICE_CODE_MAX_ATTEMPTS; attempt++) {
1069
+ let resp;
1070
+ try {
1071
+ resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
1072
+ method: "POST",
1073
+ headers: {
1074
+ Accept: "application/json",
1075
+ "Content-Type": "application/json",
1076
+ "User-Agent": "serviceme-core"
1077
+ },
1078
+ body
1079
+ });
1080
+ } catch (error) {
1081
+ if (!isTransientNetworkError(error) || attempt === DEVICE_CODE_MAX_ATTEMPTS) {
1082
+ throw error;
1083
+ }
1084
+ lastNetworkError = error;
1085
+ await sleep(this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1));
1086
+ continue;
1087
+ }
1088
+ if (!resp.ok) {
1089
+ throw new Error(
1090
+ `GitHub device-code request failed: HTTP ${resp.status}`
1091
+ );
1092
+ }
1093
+ const data = await resp.json();
1094
+ if (!data.device_code || !data.user_code || !data.verification_uri) {
1095
+ throw new Error("GitHub device-code response missing required fields");
1096
+ }
1097
+ return {
1098
+ provider: this.providerId,
1099
+ userCode: data.user_code,
1100
+ verificationUrl: data.verification_uri,
1101
+ expiresAt: Date.now() + data.expires_in * 1e3,
1102
+ message: `Open ${data.verification_uri} and enter ${data.user_code}`
1103
+ };
1076
1104
  }
1077
- return {
1078
- provider: this.providerId,
1079
- userCode: data.user_code,
1080
- verificationUrl: data.verification_uri,
1081
- expiresAt: Date.now() + data.expires_in * 1e3,
1082
- message: `Open ${data.verification_uri} and enter ${data.user_code}`
1083
- };
1105
+ throw lastNetworkError ?? new Error("GitHub device-code request failed");
1084
1106
  }
1085
1107
  async completeDeviceFlow(deviceCode, shouldContinue) {
1086
1108
  const start = Date.now();
@@ -1098,21 +1120,36 @@ var GitHubAuthProvider = class {
1098
1120
  if (shouldContinue && !shouldContinue()) {
1099
1121
  throw new Error("GitHub device flow cancelled by caller");
1100
1122
  }
1101
- const resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
1102
- method: "POST",
1103
- headers: {
1104
- Accept: "application/json",
1105
- "Content-Type": "application/json",
1106
- "User-Agent": "serviceme-core"
1107
- },
1108
- body: JSON.stringify({
1109
- client_id: this.cfg.clientId,
1110
- device_code: deviceCode,
1111
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1112
- })
1113
- });
1123
+ let resp;
1124
+ try {
1125
+ resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
1126
+ method: "POST",
1127
+ headers: {
1128
+ Accept: "application/json",
1129
+ "Content-Type": "application/json",
1130
+ "User-Agent": "serviceme-core"
1131
+ },
1132
+ body: JSON.stringify({
1133
+ client_id: this.cfg.clientId,
1134
+ device_code: deviceCode,
1135
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1136
+ })
1137
+ });
1138
+ } catch (error) {
1139
+ if (!isTransientNetworkError(error)) {
1140
+ throw error;
1141
+ }
1142
+ pollIntervalMs = Math.min(
1143
+ Math.round(pollIntervalMs * 1.5),
1144
+ this.cfg.maxPollIntervalMs
1145
+ );
1146
+ continue;
1147
+ }
1114
1148
  if (!resp.ok) {
1115
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1149
+ pollIntervalMs = Math.min(
1150
+ Math.round(pollIntervalMs * 1.5),
1151
+ this.cfg.maxPollIntervalMs
1152
+ );
1116
1153
  continue;
1117
1154
  }
1118
1155
  const data = await resp.json();
@@ -1138,7 +1175,9 @@ var GitHubAuthProvider = class {
1138
1175
  if (data.error === "slow_down") {
1139
1176
  consecutiveSlowDown++;
1140
1177
  pollIntervalMs = Math.min(
1141
- Math.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)),
1178
+ Math.round(
1179
+ pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)
1180
+ ),
1142
1181
  this.cfg.maxPollIntervalMs
1143
1182
  );
1144
1183
  continue;
@@ -1174,7 +1213,9 @@ var GitHubAuthProvider = class {
1174
1213
  }
1175
1214
  const data = await resp.json();
1176
1215
  if (!data.access_token) {
1177
- throw new Error(`GitHub refresh failed: ${data.error ?? "no_access_token"}`);
1216
+ throw new Error(
1217
+ `GitHub refresh failed: ${data.error ?? "no_access_token"}`
1218
+ );
1178
1219
  }
1179
1220
  const rawUser = await this.fetchGitHubUser(data.access_token);
1180
1221
  const email = await this.resolveEmail(data.access_token, rawUser);
@@ -1244,13 +1285,16 @@ var GitHubAuthProvider = class {
1244
1285
  */
1245
1286
  async resolveEmail(token, user) {
1246
1287
  if (user.email) return user.email;
1247
- const emailsResp = await this.cfg.fetchImpl("https://api.github.com/user/emails", {
1248
- headers: {
1249
- Accept: "application/vnd.github+json",
1250
- Authorization: `Bearer ${token}`,
1251
- "User-Agent": "serviceme-core"
1288
+ const emailsResp = await this.cfg.fetchImpl(
1289
+ "https://api.github.com/user/emails",
1290
+ {
1291
+ headers: {
1292
+ Accept: "application/vnd.github+json",
1293
+ Authorization: `Bearer ${token}`,
1294
+ "User-Agent": "serviceme-core"
1295
+ }
1252
1296
  }
1253
- });
1297
+ );
1254
1298
  if (!emailsResp.ok) {
1255
1299
  return null;
1256
1300
  }
@@ -2284,14 +2328,7 @@ function extractFrontmatter(raw) {
2284
2328
  }
2285
2329
  return { data, body };
2286
2330
  }
2287
- var SKIP_DIRS = /* @__PURE__ */ new Set([
2288
- ".git",
2289
- "node_modules",
2290
- ".vscode",
2291
- "dist",
2292
- "build",
2293
- "out"
2294
- ]);
2331
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".vscode", "dist", "build", "out"]);
2295
2332
  var SKIP_TOP_LEVEL_SCAN_DIRS = /* @__PURE__ */ new Set([...SKIP_DIRS, "plugins"]);
2296
2333
 
2297
2334
  // src/drafts/index.ts
@@ -3552,7 +3589,9 @@ function idFromUrl(url) {
3552
3589
  const last = trimmed.split("/").pop() ?? "";
3553
3590
  const safe = last.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
3554
3591
  if (!SAFE_REPO_ID_PATTERN.test(safe)) {
3555
- throw new InvalidRepoUrlError(`cannot derive a valid repo id from url: ${url}`);
3592
+ throw new InvalidRepoUrlError(
3593
+ `cannot derive a valid repo id from url: ${url}`
3594
+ );
3556
3595
  }
3557
3596
  return safe;
3558
3597
  }
@@ -3594,7 +3633,10 @@ var RepoManager = class {
3594
3633
  if (!repo.enabled) continue;
3595
3634
  const localPath = getRepoDir(repo.id);
3596
3635
  const exists = await this.pathExists(localPath);
3597
- if (exists) continue;
3636
+ if (exists) {
3637
+ if (await this.isValidGitRepo(localPath)) continue;
3638
+ await fs8.rm(localPath, { recursive: true, force: true });
3639
+ }
3598
3640
  try {
3599
3641
  if (!this.skipClone) {
3600
3642
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
@@ -3629,7 +3671,10 @@ var RepoManager = class {
3629
3671
  if (!repo) throw new RepoNotInStoreError(repoId);
3630
3672
  const localPath = getRepoDir(repoId);
3631
3673
  const exists = await this.pathExists(localPath);
3632
- if (!exists) {
3674
+ if (exists && !await this.isValidGitRepo(localPath)) {
3675
+ await fs8.rm(localPath, { recursive: true, force: true });
3676
+ }
3677
+ if (!exists || !await this.pathExists(localPath)) {
3633
3678
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3634
3679
  if (!this.skipClone) {
3635
3680
  await this.git.clone(repoId, repo.url, localPath);
@@ -3695,7 +3740,9 @@ var RepoManager = class {
3695
3740
  async addUserRepo(input) {
3696
3741
  const url = input.url.trim();
3697
3742
  if (!/^https?:\/\//.test(url) && !url.startsWith("git@")) {
3698
- throw new InvalidRepoUrlError(`expected https:// or git@ URL, got: ${url}`);
3743
+ throw new InvalidRepoUrlError(
3744
+ `expected https:// or git@ URL, got: ${url}`
3745
+ );
3699
3746
  }
3700
3747
  const id = idFromUrl(url);
3701
3748
  assertSafeRepoId(id);
@@ -3777,6 +3824,14 @@ var RepoManager = class {
3777
3824
  async ensureHome() {
3778
3825
  await fs8.mkdir(getServicemeHome(), { recursive: true });
3779
3826
  }
3827
+ /**
3828
+ * Returns `true` when `p` contains a `.git` entry — i.e. it is an
3829
+ * initialised git repository and safe to `pull`. Empty directories
3830
+ * (e.g. from an interrupted clone) return `false`.
3831
+ */
3832
+ async isValidGitRepo(p) {
3833
+ return this.pathExists(path11.join(p, ".git"));
3834
+ }
3780
3835
  async pathExists(p) {
3781
3836
  try {
3782
3837
  await fs8.stat(p);
@@ -5949,7 +6004,10 @@ function readV1Config(v1Path) {
5949
6004
  try {
5950
6005
  raw = fs19.readFileSync(v1Path, "utf-8");
5951
6006
  } catch (err) {
5952
- return { ok: false, error: `read failed: ${err instanceof Error ? err.message : String(err)}` };
6007
+ return {
6008
+ ok: false,
6009
+ error: `read failed: ${err instanceof Error ? err.message : String(err)}`
6010
+ };
5953
6011
  }
5954
6012
  let parsed;
5955
6013
  try {
@@ -6004,79 +6062,77 @@ function disambiguateName(task, existingNames, workspaceName) {
6004
6062
  existingNames.add(candidate);
6005
6063
  return { ...task, name: candidate };
6006
6064
  }
6007
- var MigrateToGlobal = class {
6008
- /**
6009
- * Run the migration. Idempotent: each v1 file is deleted after processing,
6010
- * so re-running this method on a partially-migrated state is safe (the
6011
- * already-migrated files no longer exist; only the remainder is processed).
6012
- */
6013
- static async run(options) {
6014
- const globalConfigPath = options.globalConfigPath ?? getScheduledTasksConfigPath();
6015
- const migrationFailuresPath = options.migrationFailuresPath ?? getMigrationFailuresPath();
6016
- const probe = options.probe ?? defaultProbe;
6017
- const existing = readJsonFile(globalConfigPath);
6018
- const baseConfig = existing && existing.version === 2 ? existing : { version: 2, tasks: [] };
6019
- const existingNames = new Set(baseConfig.tasks.map((t) => t.name));
6020
- const priorFailures = readJsonFile(migrationFailuresPath) ?? [];
6021
- const failures = [...priorFailures];
6022
- let migrated = 0;
6023
- const conflicts = [];
6024
- const issues = [];
6025
- for (const workspacePath of options.workspacePaths) {
6026
- const v1Path = path20.join(workspacePath, WORKSPACE_DIR, V1_FILENAME);
6027
- if (!fs19.existsSync(v1Path)) continue;
6028
- const v1 = readV1Config(v1Path);
6029
- if (!v1.ok) {
6030
- failures.push({
6031
- workspacePath,
6032
- v1Path,
6033
- error: v1.error,
6034
- at: (/* @__PURE__ */ new Date()).toISOString()
6035
- });
6036
- safeDelete(v1Path);
6037
- continue;
6038
- }
6039
- let workspaceRef;
6040
- try {
6041
- workspaceRef = await probe(workspacePath);
6042
- } catch (err) {
6043
- failures.push({
6044
- workspacePath,
6045
- v1Path,
6046
- error: `probe failed: ${err instanceof Error ? err.message : String(err)}`,
6047
- at: (/* @__PURE__ */ new Date()).toISOString()
6048
- });
6049
- safeDelete(v1Path);
6050
- continue;
6051
- }
6052
- const { config, issues: taskIssues } = (0, import_devtools_protocol9.migrateV1ToV2)(v1.config, workspaceRef);
6053
- issues.push(...taskIssues);
6054
- const newTasks = config.tasks.map((t) => {
6055
- if (existingNames.has(t.name)) {
6056
- conflicts.push(t.name);
6057
- return disambiguateName(t, existingNames, workspaceRef.name);
6058
- }
6059
- existingNames.add(t.name);
6060
- return t;
6065
+ async function migrateToGlobal(options) {
6066
+ const globalConfigPath = options.globalConfigPath ?? getScheduledTasksConfigPath();
6067
+ const migrationFailuresPath = options.migrationFailuresPath ?? getMigrationFailuresPath();
6068
+ const probe = options.probe ?? defaultProbe;
6069
+ const existing = readJsonFile(globalConfigPath);
6070
+ const baseConfig = existing && existing.version === 2 ? existing : { version: 2, tasks: [] };
6071
+ const existingNames = new Set(baseConfig.tasks.map((t) => t.name));
6072
+ const priorFailures = readJsonFile(migrationFailuresPath) ?? [];
6073
+ const failures = [...priorFailures];
6074
+ let migrated = 0;
6075
+ const conflicts = [];
6076
+ const issues = [];
6077
+ for (const workspacePath of options.workspacePaths) {
6078
+ const v1Path = path20.join(workspacePath, WORKSPACE_DIR, V1_FILENAME);
6079
+ if (!fs19.existsSync(v1Path)) continue;
6080
+ const v1 = readV1Config(v1Path);
6081
+ if (!v1.ok) {
6082
+ failures.push({
6083
+ workspacePath,
6084
+ v1Path,
6085
+ error: v1.error,
6086
+ at: (/* @__PURE__ */ new Date()).toISOString()
6061
6087
  });
6062
- baseConfig.tasks.push(...newTasks);
6063
- migrated += 1;
6064
6088
  safeDelete(v1Path);
6089
+ continue;
6065
6090
  }
6066
- if (migrated > 0) {
6067
- ensureDir(globalConfigPath);
6068
- const tmp = `${globalConfigPath}.tmp`;
6069
- fs19.writeFileSync(tmp, JSON.stringify(baseConfig, null, " "), "utf-8");
6070
- fs19.renameSync(tmp, globalConfigPath);
6071
- }
6072
- if (failures.length > priorFailures.length) {
6073
- writeJsonFile(migrationFailuresPath, failures);
6074
- } else if (failures.length === 0 && priorFailures.length > 0) {
6075
- safeDelete(migrationFailuresPath);
6091
+ let workspaceRef;
6092
+ try {
6093
+ workspaceRef = await probe(workspacePath);
6094
+ } catch (err) {
6095
+ failures.push({
6096
+ workspacePath,
6097
+ v1Path,
6098
+ error: `probe failed: ${err instanceof Error ? err.message : String(err)}`,
6099
+ at: (/* @__PURE__ */ new Date()).toISOString()
6100
+ });
6101
+ safeDelete(v1Path);
6102
+ continue;
6076
6103
  }
6077
- return { migrated, failed: failures.length - priorFailures.length, conflicts, issues };
6104
+ const { config, issues: taskIssues } = (0, import_devtools_protocol9.migrateV1ToV2)(v1.config, workspaceRef);
6105
+ issues.push(...taskIssues);
6106
+ const newTasks = config.tasks.map((t) => {
6107
+ if (existingNames.has(t.name)) {
6108
+ conflicts.push(t.name);
6109
+ return disambiguateName(t, existingNames, workspaceRef.name);
6110
+ }
6111
+ existingNames.add(t.name);
6112
+ return t;
6113
+ });
6114
+ baseConfig.tasks.push(...newTasks);
6115
+ migrated += 1;
6116
+ safeDelete(v1Path);
6078
6117
  }
6079
- };
6118
+ if (migrated > 0) {
6119
+ ensureDir(globalConfigPath);
6120
+ const tmp = `${globalConfigPath}.tmp`;
6121
+ fs19.writeFileSync(tmp, JSON.stringify(baseConfig, null, " "), "utf-8");
6122
+ fs19.renameSync(tmp, globalConfigPath);
6123
+ }
6124
+ if (failures.length > priorFailures.length) {
6125
+ writeJsonFile(migrationFailuresPath, failures);
6126
+ } else if (failures.length === 0 && priorFailures.length > 0) {
6127
+ safeDelete(migrationFailuresPath);
6128
+ }
6129
+ return {
6130
+ migrated,
6131
+ failed: failures.length - priorFailures.length,
6132
+ conflicts,
6133
+ issues
6134
+ };
6135
+ }
6080
6136
 
6081
6137
  // src/scheduled-tasks/workspace-probe/WorkspaceProbe.ts
6082
6138
  var import_node_child_process5 = require("child_process");
@@ -6979,7 +7035,6 @@ var ToolboxCore = class {
6979
7035
  MIGRATION_FAILURES_FILENAME,
6980
7036
  MicrosoftAuthProvider,
6981
7037
  MicrosoftProviderNotHostedError,
6982
- MigrateToGlobal,
6983
7038
  NodeGitSpawner,
6984
7039
  PROFILES_JSON_FILENAME,
6985
7040
  PidManager,
@@ -7078,6 +7133,7 @@ var ToolboxCore = class {
7078
7133
  isUserRepo,
7079
7134
  matchesCron,
7080
7135
  mergeWithDefaults,
7136
+ migrateToGlobal,
7081
7137
  moveFiles,
7082
7138
  narrowRepoConfig,
7083
7139
  noopLogger,