@iloom/cli 0.5.2 → 0.5.4
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 +2 -0
- package/dist/README.md +2 -0
- package/dist/{chunk-66BMJ25W.js → chunk-6YSFTPKW.js} +8 -192
- package/dist/chunk-6YSFTPKW.js.map +1 -0
- package/dist/{chunk-XNNXAAZT.js → chunk-E4F7KASE.js} +2 -2
- package/dist/{chunk-HMMO2LDS.js → chunk-ESP2FF52.js} +2 -2
- package/dist/chunk-NKRQNER7.js +197 -0
- package/dist/chunk-NKRQNER7.js.map +1 -0
- package/dist/{chunk-Z5BM4JWB.js → chunk-NRDY6XO3.js} +4 -4
- package/dist/{chunk-53OMUNUN.js → chunk-TKL7RBEF.js} +7 -2
- package/dist/chunk-TKL7RBEF.js.map +1 -0
- package/dist/{chunk-YU5HVI6B.js → chunk-YPOG7WY4.js} +2 -2
- package/dist/{chunk-VV66DH6T.js → chunk-ZXWTOJXA.js} +26 -7
- package/dist/chunk-ZXWTOJXA.js.map +1 -0
- package/dist/{cleanup-Y5W3CNUV.js → cleanup-H5QUWBWE.js} +6 -6
- package/dist/cli.js +30 -30
- package/dist/cli.js.map +1 -1
- package/dist/{contribute-K7UXBOML.js → contribute-VP73TPAL.js} +3 -3
- package/dist/{contribute-K7UXBOML.js.map → contribute-VP73TPAL.js.map} +1 -1
- package/dist/{dev-server-HNBRWGCD.js → dev-server-H5FFXIVX.js} +4 -4
- package/dist/{git-OV6ADVO7.js → git-UHUNQZBA.js} +2 -2
- package/dist/{ignite-3HB3ZBEW.js → ignite-VPP4PMF4.js} +7 -5
- package/dist/{ignite-3HB3ZBEW.js.map → ignite-VPP4PMF4.js.map} +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/{init-CMIRHFSR.js → init-4ZR2XXZA.js} +5 -4
- package/dist/{open-AXE225Z5.js → open-6HBQHPUL.js} +4 -4
- package/dist/{projects-GVEMCN5R.js → projects-SA76I4TZ.js} +14 -7
- package/dist/projects-SA76I4TZ.js.map +1 -0
- package/dist/prompts/issue-prompt.txt +12 -6
- package/dist/{rebase-6UIHMUWS.js → rebase-SRBOVC4M.js} +4 -4
- package/dist/{recap-XTBNMEMO.js → recap-X7FTTKPP.js} +4 -4
- package/dist/{run-H375EYRB.js → run-5FU76FFE.js} +4 -4
- package/dist/{shell-33FJCWJQ.js → shell-UQJDI36V.js} +4 -4
- package/dist/{summary-JUMOCNLR.js → summary-MOKN7RM2.js} +3 -3
- package/dist/{test-git-CO3BA4BV.js → test-git-GB3B6QNT.js} +2 -2
- package/dist/{test-prefix-HZYSDQYT.js → test-prefix-YQNNTCY3.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-53OMUNUN.js.map +0 -1
- package/dist/chunk-66BMJ25W.js.map +0 -1
- package/dist/chunk-VV66DH6T.js.map +0 -1
- package/dist/projects-GVEMCN5R.js.map +0 -1
- /package/dist/{chunk-XNNXAAZT.js.map → chunk-E4F7KASE.js.map} +0 -0
- /package/dist/{chunk-HMMO2LDS.js.map → chunk-ESP2FF52.js.map} +0 -0
- /package/dist/{chunk-Z5BM4JWB.js.map → chunk-NRDY6XO3.js.map} +0 -0
- /package/dist/{chunk-YU5HVI6B.js.map → chunk-YPOG7WY4.js.map} +0 -0
- /package/dist/{cleanup-Y5W3CNUV.js.map → cleanup-H5QUWBWE.js.map} +0 -0
- /package/dist/{dev-server-HNBRWGCD.js.map → dev-server-H5FFXIVX.js.map} +0 -0
- /package/dist/{git-OV6ADVO7.js.map → git-UHUNQZBA.js.map} +0 -0
- /package/dist/{init-CMIRHFSR.js.map → init-4ZR2XXZA.js.map} +0 -0
- /package/dist/{open-AXE225Z5.js.map → open-6HBQHPUL.js.map} +0 -0
- /package/dist/{rebase-6UIHMUWS.js.map → rebase-SRBOVC4M.js.map} +0 -0
- /package/dist/{recap-XTBNMEMO.js.map → recap-X7FTTKPP.js.map} +0 -0
- /package/dist/{run-H375EYRB.js.map → run-5FU76FFE.js.map} +0 -0
- /package/dist/{shell-33FJCWJQ.js.map → shell-UQJDI36V.js.map} +0 -0
- /package/dist/{summary-JUMOCNLR.js.map → summary-MOKN7RM2.js.map} +0 -0
- /package/dist/{test-git-CO3BA4BV.js.map → test-git-GB3B6QNT.js.map} +0 -0
- /package/dist/{test-prefix-HZYSDQYT.js.map → test-prefix-YQNNTCY3.js.map} +0 -0
package/README.md
CHANGED
|
@@ -355,6 +355,8 @@ We (Claude and I) welcome contributions! We've made it easy to get started — i
|
|
|
355
355
|
iloom contribute # Handles forking, cloning, and setting up the dev environment automatically.
|
|
356
356
|
```
|
|
357
357
|
|
|
358
|
+
**All PRs should be created with iloom or include detailed context.** When you run `iloom contribute`, it configures iloom to create a draft PR as soon as you start work. As you work, iloom posts the AI's analysis, implementation plan, and progress directly to that draft PR—giving reviewers full context before the code is even ready for review. If you're not using iloom, please provide equivalent detail in your PR.
|
|
359
|
+
|
|
358
360
|
New contributors should start with issues labeled [starter-task](https://github.com/iloom-ai/iloom-cli/issues?q=is%3Aissue+is%3Aopen+label%3Astarter-task). For details, see our [Contributing Guide](CONTRIBUTING.md).
|
|
359
361
|
|
|
360
362
|
License & Name
|
package/dist/README.md
CHANGED
|
@@ -355,6 +355,8 @@ We (Claude and I) welcome contributions! We've made it easy to get started — i
|
|
|
355
355
|
iloom contribute # Handles forking, cloning, and setting up the dev environment automatically.
|
|
356
356
|
```
|
|
357
357
|
|
|
358
|
+
**All PRs should be created with iloom or include detailed context.** When you run `iloom contribute`, it configures iloom to create a draft PR as soon as you start work. As you work, iloom posts the AI's analysis, implementation plan, and progress directly to that draft PR—giving reviewers full context before the code is even ready for review. If you're not using iloom, please provide equivalent detail in your PR.
|
|
359
|
+
|
|
358
360
|
New contributors should start with issues labeled [starter-task](https://github.com/iloom-ai/iloom-cli/issues?q=is%3Aissue+is%3Aopen+label%3Astarter-task). For details, see our [Contributing Guide](CONTRIBUTING.md).
|
|
359
361
|
|
|
360
362
|
License & Name
|
|
@@ -224,192 +224,9 @@ var IssueTrackerFactory = class {
|
|
|
224
224
|
}
|
|
225
225
|
};
|
|
226
226
|
|
|
227
|
-
// src/utils/FirstRunManager.ts
|
|
228
|
-
import os from "os";
|
|
229
|
-
import path from "path";
|
|
230
|
-
import fs from "fs-extra";
|
|
231
|
-
var FirstRunManager = class {
|
|
232
|
-
constructor(feature = "spin") {
|
|
233
|
-
this.configDir = path.join(os.homedir(), ".config", "iloom-ai");
|
|
234
|
-
this.markerFilePath = path.join(this.configDir, `${feature}-first-run`);
|
|
235
|
-
logger.debug("FirstRunManager initialized", {
|
|
236
|
-
feature,
|
|
237
|
-
markerFilePath: this.markerFilePath
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Get the directory for project marker files
|
|
242
|
-
*/
|
|
243
|
-
getProjectsDir() {
|
|
244
|
-
return path.join(this.configDir, "projects");
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Resolve symlinks in project path to get canonical path
|
|
248
|
-
* Falls back to original path on errors (broken symlinks, permissions, etc.)
|
|
249
|
-
*/
|
|
250
|
-
async resolveProjectPath(projectPath) {
|
|
251
|
-
try {
|
|
252
|
-
return await fs.realpath(projectPath);
|
|
253
|
-
} catch {
|
|
254
|
-
logger.debug("resolveProjectPath: Failed to resolve symlink, using original path", { projectPath });
|
|
255
|
-
return projectPath;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Convert a project path to a readable filename
|
|
260
|
-
* /Users/adam/Projects/my-app -> Users__adam__Projects__my-app
|
|
261
|
-
*/
|
|
262
|
-
projectPathToFileName(projectPath) {
|
|
263
|
-
const normalized = path.normalize(projectPath);
|
|
264
|
-
return normalized.replace(/^[/\\]+/, "").replace(/[/\\]+/g, "__");
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Get full path to a project's marker file
|
|
268
|
-
*/
|
|
269
|
-
getProjectMarkerPath(projectPath) {
|
|
270
|
-
return path.join(this.getProjectsDir(), this.projectPathToFileName(projectPath));
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Extract project name from path
|
|
274
|
-
*/
|
|
275
|
-
getProjectName(projectPath) {
|
|
276
|
-
return path.basename(projectPath);
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Check if a project has been configured
|
|
280
|
-
* Returns true if project marker file exists
|
|
281
|
-
*/
|
|
282
|
-
async isProjectConfigured(projectPath) {
|
|
283
|
-
const inputPath = projectPath ?? process.cwd();
|
|
284
|
-
const resolvedPath = await this.resolveProjectPath(inputPath);
|
|
285
|
-
const markerPath = this.getProjectMarkerPath(resolvedPath);
|
|
286
|
-
logger.debug("isProjectConfigured: Checking for marker file", { markerPath });
|
|
287
|
-
try {
|
|
288
|
-
const exists = await fs.pathExists(markerPath);
|
|
289
|
-
logger.debug(`isProjectConfigured: Marker file exists=${exists}`);
|
|
290
|
-
return exists;
|
|
291
|
-
} catch (error) {
|
|
292
|
-
logger.debug(`isProjectConfigured: Error checking marker file, treating as not configured: ${error}`);
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Check if a project has local .iloom settings files
|
|
298
|
-
* Returns true if .iloom/settings.json or .iloom/settings.local.json exists with content
|
|
299
|
-
*/
|
|
300
|
-
async hasLocalSettings(projectPath) {
|
|
301
|
-
const iloomDir = path.join(projectPath, ".iloom");
|
|
302
|
-
const settingsPath = path.join(iloomDir, "settings.json");
|
|
303
|
-
const settingsLocalPath = path.join(iloomDir, "settings.local.json");
|
|
304
|
-
const hasSettings = await this.hasNonEmptySettingsFile(settingsPath);
|
|
305
|
-
const hasLocalSettings = await this.hasNonEmptySettingsFile(settingsLocalPath);
|
|
306
|
-
return hasSettings || hasLocalSettings;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Check if a settings file exists and has non-empty content
|
|
310
|
-
*/
|
|
311
|
-
async hasNonEmptySettingsFile(filePath) {
|
|
312
|
-
try {
|
|
313
|
-
const exists = await fs.pathExists(filePath);
|
|
314
|
-
if (!exists) return false;
|
|
315
|
-
const content = await fs.readFile(filePath, "utf8");
|
|
316
|
-
const parsed = JSON.parse(content);
|
|
317
|
-
return Object.keys(parsed).length > 0;
|
|
318
|
-
} catch {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Fixup legacy projects that have local config but lack the global marker file
|
|
324
|
-
* This handles projects configured before global project tracking was implemented
|
|
325
|
-
*
|
|
326
|
-
* Returns object with:
|
|
327
|
-
* - isConfigured: true if project is configured (either already had marker or fixup created one)
|
|
328
|
-
* - wasFixedUp: true if fixup was performed (marker was created by this call)
|
|
329
|
-
*
|
|
330
|
-
* This combined return avoids duplicate isProjectConfigured() calls in needsFirstRunSetup()
|
|
331
|
-
*/
|
|
332
|
-
async fixupLegacyProject(projectPath) {
|
|
333
|
-
const inputPath = projectPath ?? process.cwd();
|
|
334
|
-
const resolvedPath = await this.resolveProjectPath(inputPath);
|
|
335
|
-
logger.debug("fixupLegacyProject: Checking for legacy project", { projectPath: resolvedPath });
|
|
336
|
-
const hasMarker = await this.isProjectConfigured(resolvedPath);
|
|
337
|
-
if (hasMarker) {
|
|
338
|
-
logger.debug("fixupLegacyProject: Project already has global marker");
|
|
339
|
-
return { isConfigured: true, wasFixedUp: false };
|
|
340
|
-
}
|
|
341
|
-
const hasLocal = await this.hasLocalSettings(resolvedPath);
|
|
342
|
-
if (!hasLocal) {
|
|
343
|
-
logger.debug("fixupLegacyProject: No local settings found, not a legacy project");
|
|
344
|
-
return { isConfigured: false, wasFixedUp: false };
|
|
345
|
-
}
|
|
346
|
-
logger.debug("fixupLegacyProject: Legacy project detected, creating global marker");
|
|
347
|
-
await this.markProjectAsConfigured(resolvedPath);
|
|
348
|
-
return { isConfigured: true, wasFixedUp: true };
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Mark a project as configured
|
|
352
|
-
* Creates a marker file with project metadata
|
|
353
|
-
*/
|
|
354
|
-
async markProjectAsConfigured(projectPath) {
|
|
355
|
-
const inputPath = projectPath ?? process.cwd();
|
|
356
|
-
const resolvedPath = await this.resolveProjectPath(inputPath);
|
|
357
|
-
const markerPath = this.getProjectMarkerPath(resolvedPath);
|
|
358
|
-
logger.debug("markProjectAsConfigured: Creating marker file", { markerPath });
|
|
359
|
-
try {
|
|
360
|
-
await fs.ensureDir(this.getProjectsDir());
|
|
361
|
-
const markerContent = {
|
|
362
|
-
configuredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
363
|
-
projectPath: resolvedPath,
|
|
364
|
-
projectName: this.getProjectName(resolvedPath)
|
|
365
|
-
};
|
|
366
|
-
await fs.writeFile(markerPath, JSON.stringify(markerContent, null, 2), "utf8");
|
|
367
|
-
logger.debug("markProjectAsConfigured: Marker file created successfully");
|
|
368
|
-
} catch (error) {
|
|
369
|
-
logger.debug(`markProjectAsConfigured: Failed to create marker file: ${error}`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Check if this is the first run of the feature
|
|
374
|
-
* Returns true if marker file doesn't exist
|
|
375
|
-
* Handles errors gracefully by returning true (treat as first-run on error)
|
|
376
|
-
*/
|
|
377
|
-
async isFirstRun() {
|
|
378
|
-
logger.debug("isFirstRun: Checking for marker file", { markerFilePath: this.markerFilePath });
|
|
379
|
-
try {
|
|
380
|
-
const exists = await fs.pathExists(this.markerFilePath);
|
|
381
|
-
logger.debug(`isFirstRun: Marker file exists=${exists}`);
|
|
382
|
-
return !exists;
|
|
383
|
-
} catch (error) {
|
|
384
|
-
logger.debug(`isFirstRun: Error checking marker file, treating as first-run: ${error}`);
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Mark the feature as having been run
|
|
390
|
-
* Creates the marker file in config directory
|
|
391
|
-
* Handles errors gracefully without throwing
|
|
392
|
-
*/
|
|
393
|
-
async markAsRun() {
|
|
394
|
-
logger.debug("markAsRun: Attempting to create marker file", { markerFilePath: this.markerFilePath });
|
|
395
|
-
try {
|
|
396
|
-
const configDir = path.dirname(this.markerFilePath);
|
|
397
|
-
logger.debug(`markAsRun: Ensuring config directory exists: ${configDir}`);
|
|
398
|
-
await fs.ensureDir(configDir);
|
|
399
|
-
const markerContent = {
|
|
400
|
-
firstRun: (/* @__PURE__ */ new Date()).toISOString()
|
|
401
|
-
};
|
|
402
|
-
await fs.writeFile(this.markerFilePath, JSON.stringify(markerContent, null, 2), "utf8");
|
|
403
|
-
logger.debug("markAsRun: Marker file created successfully");
|
|
404
|
-
} catch (error) {
|
|
405
|
-
logger.debug(`markAsRun: Failed to create marker file: ${error}`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
227
|
// src/utils/mcp.ts
|
|
411
|
-
import
|
|
412
|
-
import
|
|
228
|
+
import path from "path";
|
|
229
|
+
import os from "os";
|
|
413
230
|
async function generateIssueManagementMcpConfig(contextType, repo, provider = "github", settings, draftPrNumber) {
|
|
414
231
|
var _a, _b;
|
|
415
232
|
const effectiveContextType = draftPrNumber ? "pr" : contextType;
|
|
@@ -466,7 +283,7 @@ async function generateIssueManagementMcpConfig(contextType, repo, provider = "g
|
|
|
466
283
|
issue_management: {
|
|
467
284
|
transport: "stdio",
|
|
468
285
|
command: "node",
|
|
469
|
-
args: [
|
|
286
|
+
args: [path.join(path.dirname(new globalThis.URL(import.meta.url).pathname), "../dist/mcp/issue-management-server.js")],
|
|
470
287
|
env: envVars
|
|
471
288
|
}
|
|
472
289
|
}
|
|
@@ -480,8 +297,8 @@ function slugifyPath(loomPath) {
|
|
|
480
297
|
return `${slug}.json`;
|
|
481
298
|
}
|
|
482
299
|
function generateRecapMcpConfig(loomPath, loomMetadata) {
|
|
483
|
-
const recapsDir =
|
|
484
|
-
const recapFilePath =
|
|
300
|
+
const recapsDir = path.join(os.homedir(), ".config", "iloom-ai", "recaps");
|
|
301
|
+
const recapFilePath = path.join(recapsDir, slugifyPath(loomPath));
|
|
485
302
|
const envVars = {
|
|
486
303
|
RECAP_FILE_PATH: recapFilePath,
|
|
487
304
|
LOOM_METADATA_JSON: JSON.stringify(loomMetadata)
|
|
@@ -498,8 +315,8 @@ function generateRecapMcpConfig(loomPath, loomMetadata) {
|
|
|
498
315
|
transport: "stdio",
|
|
499
316
|
command: "node",
|
|
500
317
|
args: [
|
|
501
|
-
|
|
502
|
-
|
|
318
|
+
path.join(
|
|
319
|
+
path.dirname(new globalThis.URL(import.meta.url).pathname),
|
|
503
320
|
"../dist/mcp/recap-server.js"
|
|
504
321
|
)
|
|
505
322
|
],
|
|
@@ -512,8 +329,7 @@ function generateRecapMcpConfig(loomPath, loomMetadata) {
|
|
|
512
329
|
|
|
513
330
|
export {
|
|
514
331
|
IssueTrackerFactory,
|
|
515
|
-
FirstRunManager,
|
|
516
332
|
generateIssueManagementMcpConfig,
|
|
517
333
|
generateRecapMcpConfig
|
|
518
334
|
};
|
|
519
|
-
//# sourceMappingURL=chunk-
|
|
335
|
+
//# sourceMappingURL=chunk-6YSFTPKW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/LinearService.ts","../src/lib/IssueTrackerFactory.ts","../src/utils/mcp.ts"],"sourcesContent":["/**\n * LinearService - IssueTracker implementation for Linear\n * Implements issue tracking operations using the @linear/sdk\n */\n\nimport type { Issue, PullRequest, IssueTrackerInputDetection } from '../types/index.js'\nimport type { LinearIssue } from '../types/linear.js'\nimport { LinearServiceError } from '../types/linear.js'\nimport {\n fetchLinearIssue,\n createLinearIssue,\n updateLinearIssueState,\n} from '../utils/linear.js'\nimport { promptConfirmation } from '../utils/prompt.js'\nimport type { IssueTracker } from './IssueTracker.js'\nimport { logger } from '../utils/logger.js'\n\n/**\n * Linear service configuration options\n */\nexport interface LinearServiceConfig {\n /** Linear team key (e.g., \"ENG\", \"PLAT\") */\n teamId?: string\n /** Branch naming template (e.g., \"feat/{{key}}__{{title}}\") */\n branchFormat?: string\n /** Linear API token (lin_api_...). If provided, sets process.env.LINEAR_API_TOKEN */\n apiToken?: string\n}\n\n/**\n * Linear implementation of IssueTracker interface\n */\nexport class LinearService implements IssueTracker {\n // IssueTracker interface implementation\n readonly providerName = 'linear'\n readonly supportsPullRequests = false // Linear doesn't have pull requests\n\n private config: LinearServiceConfig\n private prompter: (message: string) => Promise<boolean>\n\n constructor(\n config?: LinearServiceConfig,\n options?: { prompter?: (message: string) => Promise<boolean> },\n ) {\n this.config = config ?? {}\n this.prompter = options?.prompter ?? promptConfirmation\n\n // Set API token from config if provided (follows mcp.ts pattern)\n if (this.config.apiToken) {\n process.env.LINEAR_API_TOKEN = this.config.apiToken\n }\n }\n\n /**\n * Detect if input matches Linear identifier format (TEAM-NUMBER)\n * @param input - User input string\n * @param _repo - Repository (unused for Linear)\n * @returns Detection result with type and identifier\n */\n public async detectInputType(\n input: string,\n _repo?: string,\n ): Promise<IssueTrackerInputDetection> {\n logger.debug(`LinearService.detectInputType called with input: \"${input}\"`)\n\n // Pattern: TEAM-NUMBER (e.g., ENG-123, PLAT-456)\n // Requires at least 2 letters before dash to avoid conflict with PR-123 format\n const linearPattern = /^([A-Z]{2,}-\\d+)$/i\n const match = input.match(linearPattern)\n\n if (!match?.[1]) {\n logger.debug(`LinearService: Input \"${input}\" does not match Linear pattern`)\n return { type: 'unknown', identifier: null, rawInput: input }\n }\n\n const identifier = match[1].toUpperCase()\n logger.debug(`LinearService: Matched Linear identifier: ${identifier}`)\n\n // Validate the issue exists in Linear\n logger.debug(`LinearService: Checking if ${identifier} is a valid Linear issue via SDK`)\n const issue = await this.isValidIssue(identifier)\n\n if (issue) {\n logger.debug(`LinearService: Issue ${identifier} found: \"${issue.title}\"`)\n return { type: 'issue', identifier, rawInput: input }\n }\n\n // Not found\n logger.debug(`LinearService: Issue ${identifier} NOT found by SDK`)\n return { type: 'unknown', identifier: null, rawInput: input }\n }\n\n /**\n * Fetch a Linear issue by identifier\n * @param identifier - Linear issue identifier (string or number)\n * @param _repo - Repository (unused for Linear)\n * @returns Generic Issue type\n * @throws LinearServiceError if issue not found\n */\n public async fetchIssue(identifier: string | number, _repo?: string): Promise<Issue> {\n const linearIssue = await fetchLinearIssue(String(identifier))\n return this.mapLinearIssueToIssue(linearIssue)\n }\n\n /**\n * Check if an issue identifier is valid (silent validation)\n * @param identifier - Linear issue identifier\n * @param _repo - Repository (unused for Linear)\n * @returns Issue if valid, false if not found\n */\n public async isValidIssue(identifier: string | number, _repo?: string): Promise<Issue | false> {\n try {\n return await this.fetchIssue(identifier)\n } catch (error) {\n // Return false for NOT_FOUND errors (expected during detection)\n if (error instanceof LinearServiceError && error.code === 'NOT_FOUND') {\n return false\n }\n // Re-throw unexpected errors\n throw error\n }\n }\n\n /**\n * Validate issue state and prompt user if closed\n * @param issue - Issue to validate\n * @throws LinearServiceError if user cancels due to closed issue\n */\n public async validateIssueState(issue: Issue): Promise<void> {\n if (issue.state === 'closed') {\n const shouldContinue = await this.prompter(\n `Issue ${issue.number} is closed. Continue anyway?`,\n )\n\n if (!shouldContinue) {\n throw new LinearServiceError('INVALID_STATE', 'User cancelled due to closed issue')\n }\n }\n }\n\n /**\n * Create a new Linear issue\n * @param title - Issue title\n * @param body - Issue description (markdown)\n * @param _repository - Repository (unused for Linear)\n * @param labels - Optional label names\n * @returns Created issue identifier and URL\n * @throws LinearServiceError if teamId not configured or creation fails\n */\n public async createIssue(\n title: string,\n body: string,\n _repository?: string,\n labels?: string[],\n ): Promise<{ number: string | number; url: string }> {\n // Require teamId configuration\n if (!this.config.teamId) {\n throw new LinearServiceError(\n 'INVALID_STATE',\n 'Linear teamId not configured. Run `il init` to configure Linear settings.',\n )\n }\n\n logger.info(`Creating Linear issue in team ${this.config.teamId}: ${title}`)\n\n const result = await createLinearIssue(title, body, this.config.teamId, labels)\n\n return {\n number: result.identifier,\n url: result.url,\n }\n }\n\n /**\n * Get the web URL for a Linear issue\n * @param identifier - Linear issue identifier\n * @param _repo - Repository (unused for Linear)\n * @returns Issue URL\n */\n public async getIssueUrl(identifier: string | number, _repo?: string): Promise<string> {\n const issue = await this.fetchIssue(identifier)\n return issue.url\n }\n\n /**\n * Move a Linear issue to \"In Progress\" state\n * @param identifier - Linear issue identifier\n * @throws LinearServiceError if state update fails\n */\n public async moveIssueToInProgress(identifier: string | number): Promise<void> {\n logger.info(`Moving Linear issue ${identifier} to In Progress`)\n await updateLinearIssueState(String(identifier), 'In Progress')\n }\n\n /**\n * Extract issue context for AI prompts\n * @param entity - Issue (Linear doesn't have PRs)\n * @returns Formatted context string\n */\n public extractContext(entity: Issue | PullRequest): string {\n // Linear doesn't have PRs, always an issue\n const issue = entity as Issue\n return `Linear Issue ${issue.number}: ${issue.title}\\nState: ${issue.state}\\n\\n${issue.body}`\n }\n\n /**\n * Map Linear API issue to generic Issue type\n * @param linear - Linear issue from SDK\n * @returns Generic Issue type\n */\n private mapLinearIssueToIssue(linear: LinearIssue): Issue {\n return {\n number: linear.identifier, // Keep as string (e.g., \"ENG-123\")\n title: linear.title,\n body: linear.description ?? '',\n state: linear.state ? (linear.state.toLowerCase().includes('done') || linear.state.toLowerCase().includes('completed') || linear.state.toLowerCase().includes('canceled') ? 'closed' : 'open') : 'open',\n labels: [],\n assignees: [],\n url: linear.url,\n }\n }\n}\n","// IssueTrackerFactory - creates appropriate IssueTracker based on settings\n// Follows pattern from database provider instantiation\n\nimport type { IssueTracker } from './IssueTracker.js'\nimport { GitHubService } from './GitHubService.js'\nimport { LinearService, type LinearServiceConfig } from './LinearService.js'\nimport type { IloomSettings } from './SettingsManager.js'\nimport { getLogger } from '../utils/logger-context.js'\n\nexport type IssueTrackerProviderType = 'github' | 'linear'\n\n/**\n * Factory for creating IssueTracker instances based on settings\n * Provides a single point of provider instantiation\n *\n * Usage:\n * const tracker = IssueTrackerFactory.create(settings, { useClaude: true })\n * const issue = await tracker.fetchIssue(123)\n */\nexport class IssueTrackerFactory {\n\t/**\n\t * Create an IssueTracker instance based on settings configuration\n\t * Defaults to GitHub if no provider specified\n\t *\n\t * @param settings - iloom settings containing issueManagement.provider\n\t * @returns IssueTracker instance configured for the specified provider\n\t * @throws Error if provider type is not supported\n\t */\n\tstatic create(settings: IloomSettings): IssueTracker {\n\t\tconst provider = settings.issueManagement?.provider ?? 'github'\n\n\t\tgetLogger().debug(`IssueTrackerFactory: Creating tracker for provider \"${provider}\"`)\n\t\tgetLogger().debug(`IssueTrackerFactory: issueManagement settings:`, JSON.stringify(settings.issueManagement, null, 2))\n\n\t\tswitch (provider) {\n\t\t\tcase 'github':\n\t\t\t\tgetLogger().debug('IssueTrackerFactory: Creating GitHubService')\n\t\t\t\treturn new GitHubService()\n\t\t\tcase 'linear': {\n\t\t\t\tconst linearSettings = settings.issueManagement?.linear\n\t\t\t\tconst linearConfig: LinearServiceConfig = {}\n\n\t\t\t\tif (linearSettings?.teamId) {\n\t\t\t\t\tlinearConfig.teamId = linearSettings.teamId\n\t\t\t\t}\n\t\t\t\tif (linearSettings?.branchFormat) {\n\t\t\t\t\tlinearConfig.branchFormat = linearSettings.branchFormat\n\t\t\t\t}\n\t\t\t\tif (linearSettings?.apiToken) {\n\t\t\t\t\tlinearConfig.apiToken = linearSettings.apiToken\n\t\t\t\t}\n\n\t\t\t\tgetLogger().debug(`IssueTrackerFactory: Creating LinearService with config:`, JSON.stringify(linearConfig, null, 2))\n\t\t\t\treturn new LinearService(linearConfig)\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unsupported issue tracker provider: ${provider}`)\n\t\t}\n\t}\n\n\t/**\n\t * Get the configured provider name from settings\n\t * Defaults to 'github' if not configured\n\t *\n\t * @param settings - iloom settings\n\t * @returns Provider type string\n\t */\n\tstatic getProviderName(settings: IloomSettings): IssueTrackerProviderType {\n\t\treturn (settings.issueManagement?.provider ?? 'github') as IssueTrackerProviderType\n\t}\n}\n","import path from 'path'\nimport os from 'os'\nimport { getRepoInfo } from './github.js'\nimport { logger } from './logger.js'\nimport type { IloomSettings } from '../lib/SettingsManager.js'\nimport type { LoomMetadata } from '../lib/MetadataManager.js'\n\n/**\n * Generate MCP configuration for issue management\n * Uses a single server that can handle both issues and pull requests\n * Returns array of MCP server config objects\n * @param contextType - Optional context type (issue or pr)\n * @param repo - Optional repo in \"owner/repo\" format. If not provided, will auto-detect from git.\n * @param provider - Issue management provider (default: 'github')\n * @param settings - Optional settings to extract Linear API token from\n * @param draftPrNumber - Optional draft PR number for github-draft-pr mode (routes comments to PR)\n */\nexport async function generateIssueManagementMcpConfig(\n\tcontextType?: 'issue' | 'pr',\n\trepo?: string,\n\tprovider: 'github' | 'linear' = 'github',\n\tsettings?: IloomSettings,\n\tdraftPrNumber?: number\n): Promise<Record<string, unknown>[]> {\n\t// When draftPrNumber is provided (github-draft-pr mode), force contextType to 'pr'\n\t// This ensures agents route comments to the draft PR instead of the issue\n\tconst effectiveContextType = draftPrNumber ? 'pr' : contextType\n\n\t// Build provider-specific environment variables\n\tlet envVars: Record<string, string> = {\n\t\tISSUE_PROVIDER: provider,\n\t}\n\n\t// Add draft PR number to env vars if provided\n\tif (draftPrNumber) {\n\t\tenvVars.DRAFT_PR_NUMBER = String(draftPrNumber)\n\t}\n\n\tif (provider === 'github') {\n\t\t// Get repository information for GitHub - either from provided repo string or auto-detect\n\t\tlet owner: string\n\t\tlet name: string\n\n\t\tif (repo) {\n\t\t\tconst parts = repo.split('/')\n\t\t\tif (parts.length !== 2 || !parts[0] || !parts[1]) {\n\t\t\t\tthrow new Error(`Invalid repo format: ${repo}. Expected \"owner/repo\"`)\n\t\t\t}\n\t\t\towner = parts[0]\n\t\t\tname = parts[1]\n\t\t} else {\n\t\t\tconst repoInfo = await getRepoInfo()\n\t\t\towner = repoInfo.owner\n\t\t\tname = repoInfo.name\n\t\t}\n\n\t\t// Map logical types to GitHub's webhook event names (handle GitHub's naming quirk here)\n\t\t// Use effectiveContextType which may be overridden by draftPrNumber\n\t\tconst githubEventName = effectiveContextType === 'issue' ? 'issues' : effectiveContextType === 'pr' ? 'pull_request' : undefined\n\n\t\tenvVars = {\n\t\t\t...envVars,\n\t\t\tREPO_OWNER: owner,\n\t\t\tREPO_NAME: name,\n\t\t\tGITHUB_API_URL: 'https://api.github.com/',\n\t\t\t...(githubEventName && { GITHUB_EVENT_NAME: githubEventName }),\n\t\t}\n\n\t\tlogger.debug('Generated MCP config for GitHub issue management', {\n\t\t\tprovider,\n\t\t\trepoOwner: owner,\n\t\t\trepoName: name,\n\t\t\tcontextType: effectiveContextType ?? 'auto-detect',\n\t\t\tgithubEventName: githubEventName ?? 'auto-detect',\n\t\t\tdraftPrNumber: draftPrNumber ?? undefined,\n\t\t})\n\t} else {\n\t\t// Linear needs API token passed through\n\t\tconst apiToken = settings?.issueManagement?.linear?.apiToken ?? process.env.LINEAR_API_TOKEN\n\n\t\tif (apiToken) {\n\t\t\tenvVars.LINEAR_API_TOKEN = apiToken\n\t\t}\n\n\t\tlogger.debug('Generated MCP config for Linear issue management', {\n\t\t\tprovider,\n\t\t\thasApiToken: !!apiToken,\n\t\t\tcontextType: contextType ?? 'auto-detect',\n\t\t})\n\t}\n\n\t// Generate single MCP server config\n\tconst mcpServerConfig = {\n\t\tmcpServers: {\n\t\t\tissue_management: {\n\t\t\t\ttransport: 'stdio',\n\t\t\t\tcommand: 'node',\n\t\t\t\targs: [path.join(path.dirname(new globalThis.URL(import.meta.url).pathname), '../dist/mcp/issue-management-server.js')],\n\t\t\t\tenv: envVars,\n\t\t\t},\n\t\t},\n\t}\n\n\treturn [mcpServerConfig]\n}\n\n/**\n * Reuse MetadataManager.slugifyPath() algorithm for recap file naming\n *\n * Algorithm:\n * 1. Trim trailing slashes\n * 2. Replace all path separators (/ or \\) with ___ (triple underscore)\n * 3. Replace any other non-alphanumeric characters (except _ and -) with -\n * 4. Append .json\n */\nfunction slugifyPath(loomPath: string): string {\n\tlet slug = loomPath.replace(/[/\\\\]+$/, '')\n\tslug = slug.replace(/[/\\\\]/g, '___')\n\tslug = slug.replace(/[^a-zA-Z0-9_-]/g, '-')\n\treturn `${slug}.json`\n}\n\n/**\n * Generate MCP configuration for recap server\n *\n * The recap server captures session context (goal, decisions, insights, risks, assumptions)\n * for the VS Code Loom Context Panel.\n *\n * @param loomPath - Absolute path to the loom workspace\n * @param loomMetadata - The loom metadata object (will be stringified as JSON)\n */\nexport function generateRecapMcpConfig(\n\tloomPath: string,\n\tloomMetadata: LoomMetadata\n): Record<string, unknown>[] {\n\t// Compute recap file path using slugifyPath algorithm (same as MetadataManager)\n\tconst recapsDir = path.join(os.homedir(), '.config', 'iloom-ai', 'recaps')\n\tconst recapFilePath = path.join(recapsDir, slugifyPath(loomPath))\n\n\t// Pass both env vars:\n\t// - RECAP_FILE_PATH: where to read/write recap data\n\t// - LOOM_METADATA_JSON: stringified loom metadata (parsed by MCP using LoomMetadata type)\n\tconst envVars = {\n\t\tRECAP_FILE_PATH: recapFilePath,\n\t\tLOOM_METADATA_JSON: JSON.stringify(loomMetadata),\n\t}\n\n\tlogger.debug('Generated MCP config for recap server', {\n\t\tloomPath,\n\t\trecapFilePath,\n\t\tloomMetadataDescription: loomMetadata.description,\n\t})\n\n\treturn [\n\t\t{\n\t\t\tmcpServers: {\n\t\t\t\trecap: {\n\t\t\t\t\ttransport: 'stdio',\n\t\t\t\t\tcommand: 'node',\n\t\t\t\t\targs: [\n\t\t\t\t\t\tpath.join(\n\t\t\t\t\t\t\tpath.dirname(new globalThis.URL(import.meta.url).pathname),\n\t\t\t\t\t\t\t'../dist/mcp/recap-server.js'\n\t\t\t\t\t\t),\n\t\t\t\t\t],\n\t\t\t\t\tenv: envVars,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t]\n}"],"mappings":"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","names":[]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
findMainWorktreePathWithSettings,
|
|
5
5
|
findWorktreeForBranch,
|
|
6
6
|
getMergeTargetBranch
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-TKL7RBEF.js";
|
|
8
8
|
import {
|
|
9
9
|
SettingsManager
|
|
10
10
|
} from "./chunk-IDUICCZY.js";
|
|
@@ -369,4 +369,4 @@ To recover:
|
|
|
369
369
|
export {
|
|
370
370
|
MergeManager
|
|
371
371
|
};
|
|
372
|
-
//# sourceMappingURL=chunk-
|
|
372
|
+
//# sourceMappingURL=chunk-E4F7KASE.js.map
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
isPRBranch,
|
|
12
12
|
isValidGitRepo,
|
|
13
13
|
parseWorktreeList
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-TKL7RBEF.js";
|
|
15
15
|
import {
|
|
16
16
|
getLogger
|
|
17
17
|
} from "./chunk-6MLEBAYZ.js";
|
|
@@ -388,4 +388,4 @@ var GitWorktreeManager = class {
|
|
|
388
388
|
export {
|
|
389
389
|
GitWorktreeManager
|
|
390
390
|
};
|
|
391
|
-
//# sourceMappingURL=chunk-
|
|
391
|
+
//# sourceMappingURL=chunk-ESP2FF52.js.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
logger
|
|
4
|
+
} from "./chunk-VT4PDUYT.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/FirstRunManager.ts
|
|
7
|
+
import os from "os";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import fs from "fs-extra";
|
|
10
|
+
var FirstRunManager = class {
|
|
11
|
+
constructor(feature = "spin") {
|
|
12
|
+
this.configDir = path.join(os.homedir(), ".config", "iloom-ai");
|
|
13
|
+
this.markerFilePath = path.join(this.configDir, `${feature}-first-run`);
|
|
14
|
+
logger.debug("FirstRunManager initialized", {
|
|
15
|
+
feature,
|
|
16
|
+
markerFilePath: this.markerFilePath
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get the directory for project marker files
|
|
21
|
+
*/
|
|
22
|
+
getProjectsDir() {
|
|
23
|
+
return path.join(this.configDir, "projects");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve symlinks in project path to get canonical path
|
|
27
|
+
* Falls back to original path on errors (broken symlinks, permissions, etc.)
|
|
28
|
+
*/
|
|
29
|
+
async resolveProjectPath(projectPath) {
|
|
30
|
+
try {
|
|
31
|
+
return await fs.realpath(projectPath);
|
|
32
|
+
} catch {
|
|
33
|
+
logger.debug("resolveProjectPath: Failed to resolve symlink, using original path", { projectPath });
|
|
34
|
+
return projectPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Convert a project path to a readable filename
|
|
39
|
+
* /Users/adam/Projects/my-app -> Users__adam__Projects__my-app
|
|
40
|
+
*/
|
|
41
|
+
projectPathToFileName(projectPath) {
|
|
42
|
+
const normalized = path.normalize(projectPath);
|
|
43
|
+
return normalized.replace(/^[/\\]+/, "").replace(/[/\\]+/g, "__");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get full path to a project's marker file
|
|
47
|
+
*/
|
|
48
|
+
getProjectMarkerPath(projectPath) {
|
|
49
|
+
return path.join(this.getProjectsDir(), this.projectPathToFileName(projectPath));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract project name from path
|
|
53
|
+
*/
|
|
54
|
+
getProjectName(projectPath) {
|
|
55
|
+
return path.basename(projectPath);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a project has been configured
|
|
59
|
+
* Returns true if project marker file exists
|
|
60
|
+
*/
|
|
61
|
+
async isProjectConfigured(projectPath) {
|
|
62
|
+
const inputPath = projectPath ?? process.cwd();
|
|
63
|
+
const resolvedPath = await this.resolveProjectPath(inputPath);
|
|
64
|
+
const markerPath = this.getProjectMarkerPath(resolvedPath);
|
|
65
|
+
logger.debug("isProjectConfigured: Checking for marker file", { markerPath });
|
|
66
|
+
try {
|
|
67
|
+
const exists = await fs.pathExists(markerPath);
|
|
68
|
+
logger.debug(`isProjectConfigured: Marker file exists=${exists}`);
|
|
69
|
+
return exists;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.debug(`isProjectConfigured: Error checking marker file, treating as not configured: ${error}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check if a project has local .iloom settings files
|
|
77
|
+
* Returns true if .iloom/settings.json or .iloom/settings.local.json exists with content
|
|
78
|
+
*/
|
|
79
|
+
async hasLocalSettings(projectPath) {
|
|
80
|
+
const iloomDir = path.join(projectPath, ".iloom");
|
|
81
|
+
const settingsPath = path.join(iloomDir, "settings.json");
|
|
82
|
+
const settingsLocalPath = path.join(iloomDir, "settings.local.json");
|
|
83
|
+
const hasSettings = await this.hasNonEmptySettingsFile(settingsPath);
|
|
84
|
+
const hasLocalSettings = await this.hasNonEmptySettingsFile(settingsLocalPath);
|
|
85
|
+
return hasSettings || hasLocalSettings;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if a settings file exists and has non-empty content
|
|
89
|
+
*/
|
|
90
|
+
async hasNonEmptySettingsFile(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
const exists = await fs.pathExists(filePath);
|
|
93
|
+
if (!exists) return false;
|
|
94
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
95
|
+
const parsed = JSON.parse(content);
|
|
96
|
+
return Object.keys(parsed).length > 0;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Fixup legacy projects that have local config but lack the global marker file
|
|
103
|
+
* This handles projects configured before global project tracking was implemented
|
|
104
|
+
*
|
|
105
|
+
* Returns object with:
|
|
106
|
+
* - isConfigured: true if project is configured (either already had marker or fixup created one)
|
|
107
|
+
* - wasFixedUp: true if fixup was performed (marker was created by this call)
|
|
108
|
+
*
|
|
109
|
+
* This combined return avoids duplicate isProjectConfigured() calls in needsFirstRunSetup()
|
|
110
|
+
*/
|
|
111
|
+
async fixupLegacyProject(projectPath) {
|
|
112
|
+
const inputPath = projectPath ?? process.cwd();
|
|
113
|
+
const resolvedPath = await this.resolveProjectPath(inputPath);
|
|
114
|
+
logger.debug("fixupLegacyProject: Checking for legacy project", { projectPath: resolvedPath });
|
|
115
|
+
const hasMarker = await this.isProjectConfigured(resolvedPath);
|
|
116
|
+
if (hasMarker) {
|
|
117
|
+
logger.debug("fixupLegacyProject: Project already has global marker");
|
|
118
|
+
return { isConfigured: true, wasFixedUp: false };
|
|
119
|
+
}
|
|
120
|
+
const hasLocal = await this.hasLocalSettings(resolvedPath);
|
|
121
|
+
if (!hasLocal) {
|
|
122
|
+
logger.debug("fixupLegacyProject: No local settings found, not a legacy project");
|
|
123
|
+
return { isConfigured: false, wasFixedUp: false };
|
|
124
|
+
}
|
|
125
|
+
logger.debug("fixupLegacyProject: Legacy project detected, creating global marker");
|
|
126
|
+
await this.markProjectAsConfigured(resolvedPath);
|
|
127
|
+
return { isConfigured: true, wasFixedUp: true };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Mark a project as configured
|
|
131
|
+
* Creates a marker file with project metadata
|
|
132
|
+
* Idempotent - skips if already configured
|
|
133
|
+
*/
|
|
134
|
+
async markProjectAsConfigured(projectPath) {
|
|
135
|
+
const inputPath = projectPath ?? process.cwd();
|
|
136
|
+
const resolvedPath = await this.resolveProjectPath(inputPath);
|
|
137
|
+
const markerPath = this.getProjectMarkerPath(resolvedPath);
|
|
138
|
+
if (await this.isProjectConfigured(resolvedPath)) {
|
|
139
|
+
logger.debug("markProjectAsConfigured: Project already configured, skipping", { markerPath });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
logger.debug("markProjectAsConfigured: Creating marker file", { markerPath });
|
|
143
|
+
try {
|
|
144
|
+
await fs.ensureDir(this.getProjectsDir());
|
|
145
|
+
const markerContent = {
|
|
146
|
+
configuredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
147
|
+
projectPath: resolvedPath,
|
|
148
|
+
projectName: this.getProjectName(resolvedPath)
|
|
149
|
+
};
|
|
150
|
+
await fs.writeFile(markerPath, JSON.stringify(markerContent, null, 2), "utf8");
|
|
151
|
+
logger.debug("markProjectAsConfigured: Marker file created successfully");
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.debug(`markProjectAsConfigured: Failed to create marker file: ${error}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if this is the first run of the feature
|
|
158
|
+
* Returns true if marker file doesn't exist
|
|
159
|
+
* Handles errors gracefully by returning true (treat as first-run on error)
|
|
160
|
+
*/
|
|
161
|
+
async isFirstRun() {
|
|
162
|
+
logger.debug("isFirstRun: Checking for marker file", { markerFilePath: this.markerFilePath });
|
|
163
|
+
try {
|
|
164
|
+
const exists = await fs.pathExists(this.markerFilePath);
|
|
165
|
+
logger.debug(`isFirstRun: Marker file exists=${exists}`);
|
|
166
|
+
return !exists;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.debug(`isFirstRun: Error checking marker file, treating as first-run: ${error}`);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Mark the feature as having been run
|
|
174
|
+
* Creates the marker file in config directory
|
|
175
|
+
* Handles errors gracefully without throwing
|
|
176
|
+
*/
|
|
177
|
+
async markAsRun() {
|
|
178
|
+
logger.debug("markAsRun: Attempting to create marker file", { markerFilePath: this.markerFilePath });
|
|
179
|
+
try {
|
|
180
|
+
const configDir = path.dirname(this.markerFilePath);
|
|
181
|
+
logger.debug(`markAsRun: Ensuring config directory exists: ${configDir}`);
|
|
182
|
+
await fs.ensureDir(configDir);
|
|
183
|
+
const markerContent = {
|
|
184
|
+
firstRun: (/* @__PURE__ */ new Date()).toISOString()
|
|
185
|
+
};
|
|
186
|
+
await fs.writeFile(this.markerFilePath, JSON.stringify(markerContent, null, 2), "utf8");
|
|
187
|
+
logger.debug("markAsRun: Marker file created successfully");
|
|
188
|
+
} catch (error) {
|
|
189
|
+
logger.debug(`markAsRun: Failed to create marker file: ${error}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
FirstRunManager
|
|
196
|
+
};
|
|
197
|
+
//# sourceMappingURL=chunk-NKRQNER7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/FirstRunManager.ts"],"sourcesContent":["import os from 'os'\nimport path from 'path'\nimport fs from 'fs-extra'\nimport { logger } from './logger.js'\n\n/**\n * FirstRunManager: Detect and track first-time spin usage via marker file\n *\n * Follows the same pattern as UpdateNotifier for file-based state tracking\n * in ~/.config/iloom-ai/ directory.\n *\n * Also supports project-level tracking for config wizard completion using\n * individual marker files per project in ~/.config/iloom-ai/projects/\n */\nexport class FirstRunManager {\n\tprivate markerFilePath: string\n\tprivate configDir: string\n\n\tconstructor(feature: string = 'spin') {\n\t\tthis.configDir = path.join(os.homedir(), '.config', 'iloom-ai')\n\t\tthis.markerFilePath = path.join(this.configDir, `${feature}-first-run`)\n\t\tlogger.debug('FirstRunManager initialized', {\n\t\t\tfeature,\n\t\t\tmarkerFilePath: this.markerFilePath\n\t\t})\n\t}\n\n\t/**\n\t * Get the directory for project marker files\n\t */\n\tprivate getProjectsDir(): string {\n\t\treturn path.join(this.configDir, 'projects')\n\t}\n\n\t/**\n\t * Resolve symlinks in project path to get canonical path\n\t * Falls back to original path on errors (broken symlinks, permissions, etc.)\n\t */\n\tprivate async resolveProjectPath(projectPath: string): Promise<string> {\n\t\ttry {\n\t\t\treturn await fs.realpath(projectPath)\n\t\t} catch {\n\t\t\tlogger.debug('resolveProjectPath: Failed to resolve symlink, using original path', { projectPath })\n\t\t\treturn projectPath\n\t\t}\n\t}\n\n\t/**\n\t * Convert a project path to a readable filename\n\t * /Users/adam/Projects/my-app -> Users__adam__Projects__my-app\n\t */\n\tprivate projectPathToFileName(projectPath: string): string {\n\t\tconst normalized = path.normalize(projectPath)\n\t\treturn normalized.replace(/^[/\\\\]+/, '').replace(/[/\\\\]+/g, '__')\n\t}\n\n\t/**\n\t * Get full path to a project's marker file\n\t */\n\tprivate getProjectMarkerPath(projectPath: string): string {\n\t\treturn path.join(this.getProjectsDir(), this.projectPathToFileName(projectPath))\n\t}\n\n\t/**\n\t * Extract project name from path\n\t */\n\tprivate getProjectName(projectPath: string): string {\n\t\treturn path.basename(projectPath)\n\t}\n\n\t/**\n\t * Check if a project has been configured\n\t * Returns true if project marker file exists\n\t */\n\tasync isProjectConfigured(projectPath?: string): Promise<boolean> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tconst markerPath = this.getProjectMarkerPath(resolvedPath)\n\t\tlogger.debug('isProjectConfigured: Checking for marker file', { markerPath })\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(markerPath)\n\t\t\tlogger.debug(`isProjectConfigured: Marker file exists=${exists}`)\n\t\t\treturn exists\n\t\t} catch (error) {\n\t\t\t// On error, treat as not configured to allow wizard to run\n\t\t\tlogger.debug(`isProjectConfigured: Error checking marker file, treating as not configured: ${error}`)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Check if a project has local .iloom settings files\n\t * Returns true if .iloom/settings.json or .iloom/settings.local.json exists with content\n\t */\n\tprivate async hasLocalSettings(projectPath: string): Promise<boolean> {\n\t\tconst iloomDir = path.join(projectPath, '.iloom')\n\t\tconst settingsPath = path.join(iloomDir, 'settings.json')\n\t\tconst settingsLocalPath = path.join(iloomDir, 'settings.local.json')\n\n\t\tconst hasSettings = await this.hasNonEmptySettingsFile(settingsPath)\n\t\tconst hasLocalSettings = await this.hasNonEmptySettingsFile(settingsLocalPath)\n\n\t\treturn hasSettings || hasLocalSettings\n\t}\n\n\t/**\n\t * Check if a settings file exists and has non-empty content\n\t */\n\tprivate async hasNonEmptySettingsFile(filePath: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(filePath)\n\t\t\tif (!exists) return false\n\t\t\tconst content = await fs.readFile(filePath, 'utf8')\n\t\t\tconst parsed = JSON.parse(content)\n\t\t\treturn Object.keys(parsed).length > 0\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Fixup legacy projects that have local config but lack the global marker file\n\t * This handles projects configured before global project tracking was implemented\n\t *\n\t * Returns object with:\n\t * - isConfigured: true if project is configured (either already had marker or fixup created one)\n\t * - wasFixedUp: true if fixup was performed (marker was created by this call)\n\t *\n\t * This combined return avoids duplicate isProjectConfigured() calls in needsFirstRunSetup()\n\t */\n\tasync fixupLegacyProject(projectPath?: string): Promise<{ isConfigured: boolean; wasFixedUp: boolean }> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tlogger.debug('fixupLegacyProject: Checking for legacy project', { projectPath: resolvedPath })\n\n\t\t// Check if already has global marker - no fixup needed, but project IS configured\n\t\tconst hasMarker = await this.isProjectConfigured(resolvedPath)\n\t\tif (hasMarker) {\n\t\t\tlogger.debug('fixupLegacyProject: Project already has global marker')\n\t\t\treturn { isConfigured: true, wasFixedUp: false }\n\t\t}\n\n\t\t// Check if has local settings files - this indicates a legacy configured project\n\t\tconst hasLocal = await this.hasLocalSettings(resolvedPath)\n\t\tif (!hasLocal) {\n\t\t\tlogger.debug('fixupLegacyProject: No local settings found, not a legacy project')\n\t\t\treturn { isConfigured: false, wasFixedUp: false }\n\t\t}\n\n\t\t// Legacy project found - create the missing global marker\n\t\tlogger.debug('fixupLegacyProject: Legacy project detected, creating global marker')\n\t\tawait this.markProjectAsConfigured(resolvedPath)\n\t\treturn { isConfigured: true, wasFixedUp: true }\n\t}\n\n\t/**\n\t * Mark a project as configured\n\t * Creates a marker file with project metadata\n\t * Idempotent - skips if already configured\n\t */\n\tasync markProjectAsConfigured(projectPath?: string): Promise<void> {\n\t\tconst inputPath = projectPath ?? process.cwd()\n\t\tconst resolvedPath = await this.resolveProjectPath(inputPath)\n\t\tconst markerPath = this.getProjectMarkerPath(resolvedPath)\n\n\t\t// Idempotency check - skip if already configured\n\t\tif (await this.isProjectConfigured(resolvedPath)) {\n\t\t\tlogger.debug('markProjectAsConfigured: Project already configured, skipping', { markerPath })\n\t\t\treturn\n\t\t}\n\n\t\tlogger.debug('markProjectAsConfigured: Creating marker file', { markerPath })\n\t\ttry {\n\t\t\tawait fs.ensureDir(this.getProjectsDir())\n\t\t\tconst markerContent = {\n\t\t\t\tconfiguredAt: new Date().toISOString(),\n\t\t\t\tprojectPath: resolvedPath,\n\t\t\t\tprojectName: this.getProjectName(resolvedPath)\n\t\t\t}\n\t\t\tawait fs.writeFile(markerPath, JSON.stringify(markerContent, null, 2), 'utf8')\n\t\t\tlogger.debug('markProjectAsConfigured: Marker file created successfully')\n\t\t} catch (error) {\n\t\t\t// Don't throw on errors - just log debug message\n\t\t\tlogger.debug(`markProjectAsConfigured: Failed to create marker file: ${error}`)\n\t\t}\n\t}\n\n\t/**\n\t * Check if this is the first run of the feature\n\t * Returns true if marker file doesn't exist\n\t * Handles errors gracefully by returning true (treat as first-run on error)\n\t */\n\tasync isFirstRun(): Promise<boolean> {\n\t\tlogger.debug('isFirstRun: Checking for marker file', { markerFilePath: this.markerFilePath })\n\t\ttry {\n\t\t\tconst exists = await fs.pathExists(this.markerFilePath)\n\t\t\tlogger.debug(`isFirstRun: Marker file exists=${exists}`)\n\t\t\treturn !exists\n\t\t} catch (error) {\n\t\t\t// On error, gracefully degrade by treating as first-run\n\t\t\tlogger.debug(`isFirstRun: Error checking marker file, treating as first-run: ${error}`)\n\t\t\treturn true\n\t\t}\n\t}\n\n\t/**\n\t * Mark the feature as having been run\n\t * Creates the marker file in config directory\n\t * Handles errors gracefully without throwing\n\t */\n\tasync markAsRun(): Promise<void> {\n\t\tlogger.debug('markAsRun: Attempting to create marker file', { markerFilePath: this.markerFilePath })\n\t\ttry {\n\t\t\t// Ensure directory exists\n\t\t\tconst configDir = path.dirname(this.markerFilePath)\n\t\t\tlogger.debug(`markAsRun: Ensuring config directory exists: ${configDir}`)\n\t\t\tawait fs.ensureDir(configDir)\n\n\t\t\t// Write marker file with timestamp for debugging\n\t\t\tconst markerContent = {\n\t\t\t\tfirstRun: new Date().toISOString(),\n\t\t\t}\n\t\t\tawait fs.writeFile(this.markerFilePath, JSON.stringify(markerContent, null, 2), 'utf8')\n\t\t\tlogger.debug('markAsRun: Marker file created successfully')\n\t\t} catch (error) {\n\t\t\t// Don't throw on errors - just log debug message\n\t\t\t// Failing to write marker shouldn't break the workflow\n\t\t\tlogger.debug(`markAsRun: Failed to create marker file: ${error}`)\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
hasUncommittedChanges,
|
|
24
24
|
isBranchMergedIntoMain,
|
|
25
25
|
isFileTrackedByGit
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-TKL7RBEF.js";
|
|
27
27
|
import {
|
|
28
28
|
SettingsManager
|
|
29
29
|
} from "./chunk-IDUICCZY.js";
|
|
@@ -324,7 +324,7 @@ var LoomManager = class {
|
|
|
324
324
|
);
|
|
325
325
|
}
|
|
326
326
|
getLogger().info("Creating placeholder commit for draft PR...");
|
|
327
|
-
const { executeGitCommand: executeGitCommand2, PLACEHOLDER_COMMIT_PREFIX, pushBranchToRemote } = await import("./git-
|
|
327
|
+
const { executeGitCommand: executeGitCommand2, PLACEHOLDER_COMMIT_PREFIX, pushBranchToRemote } = await import("./git-UHUNQZBA.js");
|
|
328
328
|
await executeGitCommand2(
|
|
329
329
|
[
|
|
330
330
|
"commit",
|
|
@@ -538,7 +538,7 @@ This PR was created automatically by iloom.`;
|
|
|
538
538
|
async checkAndWarnChildLooms(branchName) {
|
|
539
539
|
let targetBranch = branchName;
|
|
540
540
|
if (!targetBranch) {
|
|
541
|
-
const { getCurrentBranch } = await import("./git-
|
|
541
|
+
const { getCurrentBranch } = await import("./git-UHUNQZBA.js");
|
|
542
542
|
targetBranch = await getCurrentBranch();
|
|
543
543
|
}
|
|
544
544
|
if (!targetBranch) {
|
|
@@ -2371,4 +2371,4 @@ export {
|
|
|
2371
2371
|
DatabaseManager,
|
|
2372
2372
|
ResourceCleanup
|
|
2373
2373
|
};
|
|
2374
|
-
//# sourceMappingURL=chunk-
|
|
2374
|
+
//# sourceMappingURL=chunk-NRDY6XO3.js.map
|
|
@@ -267,7 +267,12 @@ async function getRepoRoot(path2 = process.cwd()) {
|
|
|
267
267
|
const repoRoot = trimmedPath.replace(/\/\.git\/worktrees\/[^/]+$/, "").replace(/\/\.git$/, "");
|
|
268
268
|
return repoRoot;
|
|
269
269
|
} catch (error) {
|
|
270
|
-
|
|
270
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
271
|
+
if (errorMessage.includes("not a git repository")) {
|
|
272
|
+
logger.info(`Note: No git repository detected: ${path2}`);
|
|
273
|
+
} else {
|
|
274
|
+
logger.warn(`Failed to determine repo root from git-common-dir: ${path2}`, errorMessage);
|
|
275
|
+
}
|
|
271
276
|
return null;
|
|
272
277
|
}
|
|
273
278
|
}
|
|
@@ -703,4 +708,4 @@ export {
|
|
|
703
708
|
removePlaceholderCommitFromHead,
|
|
704
709
|
removePlaceholderCommitFromHistory
|
|
705
710
|
};
|
|
706
|
-
//# sourceMappingURL=chunk-
|
|
711
|
+
//# sourceMappingURL=chunk-TKL7RBEF.js.map
|