dlw-machine-setup 0.6.0 → 0.6.2

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.
Files changed (2) hide show
  1. package/bin/installer.js +367 -636
  2. package/package.json +1 -1
package/bin/installer.js CHANGED
@@ -2753,13 +2753,13 @@ var PromisePolyfill = class extends Promise {
2753
2753
  // Available starting from Node 22
2754
2754
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
2755
2755
  static withResolver() {
2756
- let resolve5;
2756
+ let resolve4;
2757
2757
  let reject;
2758
2758
  const promise = new Promise((res, rej) => {
2759
- resolve5 = res;
2759
+ resolve4 = res;
2760
2760
  reject = rej;
2761
2761
  });
2762
- return { promise, resolve: resolve5, reject };
2762
+ return { promise, resolve: resolve4, reject };
2763
2763
  }
2764
2764
  };
2765
2765
 
@@ -2776,7 +2776,7 @@ function createPrompt(view) {
2776
2776
  output
2777
2777
  });
2778
2778
  const screen = new ScreenManager(rl);
2779
- const { promise, resolve: resolve5, reject } = PromisePolyfill.withResolver();
2779
+ const { promise, resolve: resolve4, reject } = PromisePolyfill.withResolver();
2780
2780
  const cancel = () => reject(new CancelPromptError());
2781
2781
  if (signal) {
2782
2782
  const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
@@ -2800,7 +2800,7 @@ function createPrompt(view) {
2800
2800
  cycle(() => {
2801
2801
  try {
2802
2802
  const nextView = view(config, (value) => {
2803
- setImmediate(() => resolve5(value));
2803
+ setImmediate(() => resolve4(value));
2804
2804
  });
2805
2805
  const [content, bottomContent] = typeof nextView === "string" ? [nextView] : nextView;
2806
2806
  screen.render(content, bottomContent);
@@ -3244,14 +3244,32 @@ ${page}${helpTipBottom}${choiceDescription}${import_ansi_escapes3.default.cursor
3244
3244
  });
3245
3245
 
3246
3246
  // src/index.ts
3247
- var import_fs9 = require("fs");
3247
+ var import_fs11 = require("fs");
3248
3248
  var import_readline = require("readline");
3249
- var import_path9 = require("path");
3249
+ var import_path12 = require("path");
3250
3250
 
3251
3251
  // src/utils/fetch.ts
3252
3252
  var DEFAULT_TIMEOUT_MS = 3e4;
3253
3253
  var DEFAULT_MAX_RETRIES = 3;
3254
3254
  var DEFAULT_RETRY_DELAY_MS = 2e3;
3255
+ function getReadableError(status) {
3256
+ switch (status) {
3257
+ case 401:
3258
+ return "Authentication failed. Check your GitHub token.";
3259
+ case 403:
3260
+ return "Access denied. Verify token permissions and rate limits.";
3261
+ case 404:
3262
+ return "Resource not found. Verify the repository and release exist.";
3263
+ case 429:
3264
+ return "Rate limit exceeded. Please wait and try again.";
3265
+ case 500:
3266
+ case 502:
3267
+ case 503:
3268
+ return "GitHub server error. Please try again later.";
3269
+ default:
3270
+ return `Unexpected error (${status})`;
3271
+ }
3272
+ }
3255
3273
  async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
3256
3274
  const controller = new AbortController();
3257
3275
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -3283,40 +3301,102 @@ async function fetchWithRetry(url, options = {}, maxRetries = DEFAULT_MAX_RETRIE
3283
3301
  }
3284
3302
  if (attempt < maxRetries) {
3285
3303
  const delay = retryDelayMs * Math.pow(2, attempt - 1);
3286
- await new Promise((resolve5) => setTimeout(resolve5, delay));
3304
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
3287
3305
  }
3288
3306
  }
3289
3307
  throw lastError || new Error("Failed after maximum retries");
3290
3308
  }
3291
3309
 
3310
+ // src/utils/download-archive.ts
3311
+ var import_fs = require("fs");
3312
+ var import_path = require("path");
3313
+ var import_child_process = require("child_process");
3314
+ var MIN_FILE_SIZE = 1024;
3315
+ async function fetchLatestRelease(token, repo) {
3316
+ const headers = {
3317
+ "Accept": "application/vnd.github+json",
3318
+ "Authorization": `Bearer ${token}`
3319
+ };
3320
+ const res = await fetchWithRetry(`https://api.github.com/repos/${repo}/releases/latest`, { headers });
3321
+ if (!res.ok) {
3322
+ throw new Error(`GitHub API error (${res.status}): ${getReadableError(res.status)}`);
3323
+ }
3324
+ const data = await res.json();
3325
+ return { tagName: data.tag_name ?? "unknown", assets: data.assets ?? [] };
3326
+ }
3327
+ async function downloadAndExtractAsset(token, asset, projectPath, tempDirName) {
3328
+ const tarCheck = (0, import_child_process.spawnSync)("tar", ["--version"], { stdio: "ignore" });
3329
+ if (tarCheck.status !== 0) {
3330
+ throw new Error("tar command not found. Please ensure tar is installed and available in your PATH.");
3331
+ }
3332
+ const tempDir = (0, import_path.join)(projectPath, tempDirName);
3333
+ if ((0, import_fs.existsSync)(tempDir)) (0, import_fs.rmSync)(tempDir, { recursive: true, force: true });
3334
+ (0, import_fs.mkdirSync)(tempDir, { recursive: true });
3335
+ const cleanup = () => {
3336
+ if ((0, import_fs.existsSync)(tempDir)) {
3337
+ try {
3338
+ (0, import_fs.rmSync)(tempDir, { recursive: true, force: true });
3339
+ } catch {
3340
+ }
3341
+ }
3342
+ };
3343
+ try {
3344
+ const dlRes = await fetchWithRetry(asset.url, {
3345
+ headers: { "Accept": "application/octet-stream", "Authorization": `Bearer ${token}` }
3346
+ });
3347
+ if (!dlRes.ok) {
3348
+ throw new Error(`Download failed: ${getReadableError(dlRes.status)}`);
3349
+ }
3350
+ const archivePath = (0, import_path.join)(tempDir, asset.name);
3351
+ (0, import_fs.writeFileSync)(archivePath, Buffer.from(await dlRes.arrayBuffer()));
3352
+ if ((0, import_fs.statSync)(archivePath).size < MIN_FILE_SIZE) {
3353
+ throw new Error("File corrupted");
3354
+ }
3355
+ const list = (0, import_child_process.spawnSync)("tar", ["-tzf", archivePath], { encoding: "utf8" });
3356
+ if (list.status !== 0) throw new Error("Failed to read archive contents");
3357
+ const resolvedTemp = (0, import_path.resolve)(tempDir);
3358
+ for (const entry of list.stdout.split("\n").filter(Boolean)) {
3359
+ const path = (0, import_path.resolve)((0, import_path.join)(tempDir, entry.replace(/\/$/, "")));
3360
+ if (!path.startsWith(resolvedTemp + import_path.sep)) {
3361
+ throw new Error(`Archive contains unsafe path: ${entry}`);
3362
+ }
3363
+ }
3364
+ const ex = (0, import_child_process.spawnSync)("tar", ["-xzf", archivePath, "-C", tempDir], { stdio: "ignore" });
3365
+ if (ex.status !== 0) throw new Error("Archive extraction failed");
3366
+ return { extractedRoot: tempDir, cleanup };
3367
+ } catch (err) {
3368
+ cleanup();
3369
+ throw err;
3370
+ }
3371
+ }
3372
+
3292
3373
  // src/utils/wizard-options.ts
3293
3374
  async function fetchWizardOptions(token, repo) {
3294
- try {
3295
- const headers = {
3296
- "Accept": "application/vnd.github+json",
3375
+ const release = await fetchLatestRelease(token, repo);
3376
+ const asset = release.assets.find((a) => a.name === "wizard-options.json");
3377
+ if (!asset) {
3378
+ throw new Error("wizard-options.json not present in latest release");
3379
+ }
3380
+ const assetRes = await fetchWithRetry(asset.url, {
3381
+ headers: {
3382
+ "Accept": "application/octet-stream",
3297
3383
  "Authorization": `Bearer ${token}`
3298
- };
3299
- const releaseRes = await fetchWithRetry(
3300
- `https://api.github.com/repos/${repo}/releases/latest`,
3301
- { headers }
3302
- );
3303
- if (!releaseRes.ok) return null;
3304
- const release = await releaseRes.json();
3305
- const asset = release.assets?.find((a) => a.name === "wizard-options.json");
3306
- if (!asset) return null;
3307
- const assetRes = await fetchWithRetry(asset.url, {
3308
- headers: { ...headers, "Accept": "application/octet-stream" }
3309
- });
3310
- if (!assetRes.ok) return null;
3311
- const data = await assetRes.json();
3312
- const techArray = data.personas?.technologies ?? data.personas?.personas;
3313
- if (!Array.isArray(data.agents) || !Array.isArray(techArray)) {
3314
- return null;
3315
3384
  }
3316
- return { options: data, releaseVersion: release.tag_name ?? "unknown" };
3317
- } catch {
3318
- return null;
3385
+ });
3386
+ if (!assetRes.ok) {
3387
+ throw new Error(`wizard-options.json download failed: ${getReadableError(assetRes.status)}`);
3388
+ }
3389
+ let data;
3390
+ try {
3391
+ data = await assetRes.json();
3392
+ } catch (e) {
3393
+ throw new Error(`wizard-options.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
3394
+ }
3395
+ const techArray = data.personas?.technologies ?? data.personas?.personas;
3396
+ if (!Array.isArray(data.agents) || !Array.isArray(techArray)) {
3397
+ throw new Error("wizard-options.json is missing required arrays (agents, technologies)");
3319
3398
  }
3399
+ return { options: data, releaseVersion: release.tagName };
3320
3400
  }
3321
3401
 
3322
3402
  // src/utils/discover-repo.ts
@@ -3385,46 +3465,44 @@ async function tryOrgReposList(headers, topic) {
3385
3465
  }
3386
3466
 
3387
3467
  // src/utils/github-auth.ts
3388
- var import_fs = require("fs");
3389
- var import_path = require("path");
3468
+ var import_fs2 = require("fs");
3469
+ var import_path2 = require("path");
3390
3470
  var import_os = require("os");
3391
- var import_child_process = require("child_process");
3471
+ var import_child_process2 = require("child_process");
3392
3472
  var GITHUB_CLIENT_ID = "Ov23liwpMumAhwVufZ7N";
3393
3473
  var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
3394
3474
  var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
3475
+ var SOFT_TTL_MS = 24 * 60 * 60 * 1e3;
3395
3476
  function getTokenCachePath() {
3396
- const configDir = (0, import_path.join)((0, import_os.homedir)(), ".one-shot-installer");
3397
- if (!(0, import_fs.existsSync)(configDir)) {
3398
- (0, import_fs.mkdirSync)(configDir, { recursive: true });
3477
+ const configDir = (0, import_path2.join)((0, import_os.homedir)(), ".one-shot-installer");
3478
+ if (!(0, import_fs2.existsSync)(configDir)) {
3479
+ (0, import_fs2.mkdirSync)(configDir, { recursive: true });
3399
3480
  }
3400
- return (0, import_path.join)(configDir, "github-token.json");
3481
+ return (0, import_path2.join)(configDir, "github-token.json");
3401
3482
  }
3402
3483
  function loadCachedToken() {
3403
3484
  const cachePath = getTokenCachePath();
3404
- if (!(0, import_fs.existsSync)(cachePath)) {
3405
- return null;
3406
- }
3485
+ if (!(0, import_fs2.existsSync)(cachePath)) return null;
3407
3486
  try {
3408
- const data = (0, import_fs.readFileSync)(cachePath, "utf-8");
3409
- const cached = JSON.parse(data);
3410
- return cached.access_token;
3411
- } catch (error) {
3487
+ const data = (0, import_fs2.readFileSync)(cachePath, "utf-8");
3488
+ return JSON.parse(data);
3489
+ } catch {
3412
3490
  return null;
3413
3491
  }
3414
3492
  }
3415
3493
  function saveTokenToCache(token) {
3416
3494
  const cachePath = getTokenCachePath();
3417
- const cached = {
3495
+ const cached2 = {
3418
3496
  access_token: token,
3419
3497
  created_at: Date.now()
3420
3498
  };
3421
- (0, import_fs.writeFileSync)(cachePath, JSON.stringify(cached, null, 2), { encoding: "utf-8", mode: 384 });
3499
+ (0, import_fs2.writeFileSync)(cachePath, JSON.stringify(cached2, null, 2), { encoding: "utf-8", mode: 384 });
3422
3500
  }
3423
3501
  function clearTokenCache() {
3424
3502
  const cachePath = getTokenCachePath();
3425
- if ((0, import_fs.existsSync)(cachePath)) {
3503
+ if ((0, import_fs2.existsSync)(cachePath)) {
3426
3504
  try {
3427
- (0, import_fs.unlinkSync)(cachePath);
3505
+ (0, import_fs2.unlinkSync)(cachePath);
3428
3506
  } catch (error) {
3429
3507
  }
3430
3508
  }
@@ -3451,7 +3529,7 @@ async function pollForToken(deviceCode, interval) {
3451
3529
  const maxAttempts = 60;
3452
3530
  let attempts = 0;
3453
3531
  while (attempts < maxAttempts) {
3454
- await new Promise((resolve5) => setTimeout(resolve5, interval * 1e3));
3532
+ await new Promise((resolve4) => setTimeout(resolve4, interval * 1e3));
3455
3533
  const response = await fetchWithTimeout(GITHUB_TOKEN_URL, {
3456
3534
  method: "POST",
3457
3535
  headers: {
@@ -3499,11 +3577,14 @@ async function authenticateWithGitHub() {
3499
3577
  }
3500
3578
  console.log(" GITHUB_TOKEN env var is invalid or expired, falling back to authentication...\n");
3501
3579
  }
3502
- const cachedToken = loadCachedToken();
3503
- if (cachedToken) {
3504
- const isValid = await verifyToken(cachedToken);
3580
+ const cached2 = loadCachedToken();
3581
+ if (cached2) {
3582
+ const fresh = Date.now() - cached2.created_at < SOFT_TTL_MS;
3583
+ if (fresh) return cached2.access_token;
3584
+ const isValid = await verifyToken(cached2.access_token);
3505
3585
  if (isValid) {
3506
- return cachedToken;
3586
+ saveTokenToCache(cached2.access_token);
3587
+ return cached2.access_token;
3507
3588
  }
3508
3589
  clearTokenCache();
3509
3590
  }
@@ -3516,13 +3597,13 @@ async function authenticateWithGitHub() {
3516
3597
  try {
3517
3598
  const os = (0, import_os.platform)();
3518
3599
  if (os === "win32") {
3519
- (0, import_child_process.execSync)(`echo ${deviceCodeData.user_code} | clip`, { stdio: "ignore" });
3600
+ (0, import_child_process2.execSync)(`echo ${deviceCodeData.user_code} | clip`, { stdio: "ignore" });
3520
3601
  copied = true;
3521
3602
  } else if (os === "darwin") {
3522
- (0, import_child_process.execSync)(`echo "${deviceCodeData.user_code}" | pbcopy`, { stdio: "ignore" });
3603
+ (0, import_child_process2.execSync)(`echo "${deviceCodeData.user_code}" | pbcopy`, { stdio: "ignore" });
3523
3604
  copied = true;
3524
3605
  } else {
3525
- (0, import_child_process.execSync)(`echo "${deviceCodeData.user_code}" | xclip -selection clipboard`, { stdio: "ignore" });
3606
+ (0, import_child_process2.execSync)(`echo "${deviceCodeData.user_code}" | xclip -selection clipboard`, { stdio: "ignore" });
3526
3607
  copied = true;
3527
3608
  }
3528
3609
  } catch {
@@ -3604,11 +3685,7 @@ function getAgentTarget(agent) {
3604
3685
 
3605
3686
  // src/utils/mod.ts
3606
3687
  async function loadWizardOptions(token, repo) {
3607
- const remote = await fetchWizardOptions(token, repo);
3608
- if (!remote) {
3609
- throw new Error("Failed to load configuration from GitHub release. Check your network and token.");
3610
- }
3611
- const { options, releaseVersion } = remote;
3688
+ const { options, releaseVersion } = await fetchWizardOptions(token, repo);
3612
3689
  const filteredMcpServers = Object.fromEntries(
3613
3690
  Object.entries(options.mcpServers).filter(([, config]) => config.active !== false)
3614
3691
  );
@@ -3671,10 +3748,23 @@ __export(steps_exports, {
3671
3748
  });
3672
3749
 
3673
3750
  // src/steps/resources/fetch-contexts.ts
3674
- var import_fs2 = require("fs");
3675
- var import_path2 = require("path");
3676
- var import_child_process2 = require("child_process");
3677
- var MIN_FILE_SIZE = 1024;
3751
+ var import_fs4 = require("fs");
3752
+ var import_path4 = require("path");
3753
+
3754
+ // src/utils/fs-copy.ts
3755
+ var import_fs3 = require("fs");
3756
+ var import_path3 = require("path");
3757
+ function copyDirectory(source, target) {
3758
+ if (!(0, import_fs3.existsSync)(target)) (0, import_fs3.mkdirSync)(target, { recursive: true });
3759
+ for (const entry of (0, import_fs3.readdirSync)(source, { withFileTypes: true })) {
3760
+ const s = (0, import_path3.join)(source, entry.name);
3761
+ const t = (0, import_path3.join)(target, entry.name);
3762
+ if (entry.isDirectory()) copyDirectory(s, t);
3763
+ else (0, import_fs3.copyFileSync)(s, t);
3764
+ }
3765
+ }
3766
+
3767
+ // src/steps/resources/fetch-contexts.ts
3678
3768
  var fetch_contexts_default = defineStep({
3679
3769
  name: "fetch-contexts",
3680
3770
  label: "Downloading contexts",
@@ -3683,9 +3773,6 @@ var fetch_contexts_default = defineStep({
3683
3773
  const uniqueDomains = [...new Set(ctx.config.technologies.flatMap((p) => p.domains))];
3684
3774
  const domainValues = uniqueDomains.map((d) => d.toLowerCase());
3685
3775
  if (!ctx.contextRepo) {
3686
- for (const domain of domainValues) {
3687
- ctx.installed.domainsFailed = domainValues;
3688
- }
3689
3776
  return {
3690
3777
  status: "failed",
3691
3778
  detail: "Context repo not found (topic: context-data)"
@@ -3693,8 +3780,6 @@ var fetch_contexts_default = defineStep({
3693
3780
  }
3694
3781
  const downloadResult = await fetchContexts(domainValues, ctx.token, ctx.contextRepo, ctx.config.projectPath);
3695
3782
  ctx.installed.domainsInstalled = downloadResult.successful;
3696
- ctx.installed.domainsFailed = downloadResult.failed;
3697
- ctx.installed.failureReasons = downloadResult.failureReasons;
3698
3783
  ctx.installed.contextReleaseVersion = downloadResult.releaseVersion;
3699
3784
  const domainColWidth = Math.max(...domainValues.map((d) => d.length), 8) + 2;
3700
3785
  console.log("");
@@ -3719,24 +3804,9 @@ var fetch_contexts_default = defineStep({
3719
3804
  async function fetchContexts(domains, token, repo, targetDir) {
3720
3805
  const result = { successful: [], failed: [], failureReasons: {} };
3721
3806
  if (domains.length === 0) return result;
3722
- const tarCheck = (0, import_child_process2.spawnSync)("tar", ["--version"], { stdio: "ignore" });
3723
- if (tarCheck.status !== 0) {
3724
- throw new Error("tar command not found. Please ensure tar is installed and available in your PATH.");
3725
- }
3726
- const headers = {
3727
- "Accept": "application/vnd.github+json",
3728
- "Authorization": `Bearer ${token}`
3729
- };
3730
- const releaseResponse = await fetchWithRetry(
3731
- `https://api.github.com/repos/${repo}/releases/latest`,
3732
- { headers }
3733
- );
3734
- if (!releaseResponse.ok) {
3735
- throw new Error(`GitHub API error (${releaseResponse.status}): ${getReadableError(releaseResponse.status)}`);
3736
- }
3737
- const releaseData = await releaseResponse.json();
3738
- result.releaseVersion = releaseData.tag_name;
3739
- const asset = releaseData.assets?.find((a) => a.name.endsWith(".tar.gz"));
3807
+ const release = await fetchLatestRelease(token, repo);
3808
+ result.releaseVersion = release.tagName;
3809
+ const asset = release.assets.find((a) => a.name.endsWith(".tar.gz"));
3740
3810
  if (!asset) {
3741
3811
  for (const domain of domains) {
3742
3812
  result.failed.push(domain);
@@ -3744,39 +3814,11 @@ async function fetchContexts(domains, token, repo, targetDir) {
3744
3814
  }
3745
3815
  return result;
3746
3816
  }
3747
- const contextsDir = (0, import_path2.join)(targetDir, "_ai-context");
3748
- const tempDir = (0, import_path2.join)(targetDir, ".temp-download");
3817
+ const contextsDir = (0, import_path4.join)(targetDir, "_ai-context");
3818
+ if (!(0, import_fs4.existsSync)(contextsDir)) (0, import_fs4.mkdirSync)(contextsDir, { recursive: true });
3819
+ const archive = await downloadAndExtractAsset(token, asset, targetDir, ".temp-download");
3749
3820
  try {
3750
- if (!(0, import_fs2.existsSync)(contextsDir)) (0, import_fs2.mkdirSync)(contextsDir, { recursive: true });
3751
- if ((0, import_fs2.existsSync)(tempDir)) (0, import_fs2.rmSync)(tempDir, { recursive: true, force: true });
3752
- (0, import_fs2.mkdirSync)(tempDir, { recursive: true });
3753
- const downloadHeaders = {
3754
- "Accept": "application/octet-stream",
3755
- "Authorization": `Bearer ${token}`
3756
- };
3757
- const response = await fetchWithRetry(asset.url, { headers: downloadHeaders });
3758
- if (!response.ok) {
3759
- throw new Error(`Download failed: ${getReadableError(response.status)}`);
3760
- }
3761
- const archivePath = (0, import_path2.join)(tempDir, asset.name);
3762
- const arrayBuffer = await response.arrayBuffer();
3763
- (0, import_fs2.writeFileSync)(archivePath, Buffer.from(arrayBuffer));
3764
- const stats = (0, import_fs2.statSync)(archivePath);
3765
- if (stats.size < MIN_FILE_SIZE) {
3766
- throw new Error("File corrupted");
3767
- }
3768
- const listResult = (0, import_child_process2.spawnSync)("tar", ["-tzf", archivePath], { encoding: "utf8" });
3769
- if (listResult.status !== 0) throw new Error("Failed to read archive contents");
3770
- const resolvedTempDir = (0, import_path2.resolve)(tempDir);
3771
- for (const entry of listResult.stdout.split("\n").filter(Boolean)) {
3772
- const entryPath = (0, import_path2.resolve)((0, import_path2.join)(tempDir, entry.replace(/\/$/, "")));
3773
- if (!entryPath.startsWith(resolvedTempDir + import_path2.sep)) {
3774
- throw new Error(`Archive contains unsafe path: ${entry}`);
3775
- }
3776
- }
3777
- const extractResult = (0, import_child_process2.spawnSync)("tar", ["-xzf", archivePath, "-C", tempDir], { stdio: "ignore" });
3778
- if (extractResult.status !== 0) throw new Error("Archive extraction failed");
3779
- const extractedFolders = (0, import_fs2.readdirSync)(tempDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3821
+ const extractedFolders = (0, import_fs4.readdirSync)(archive.extractedRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3780
3822
  for (const domain of domains) {
3781
3823
  try {
3782
3824
  const match = extractedFolders.find((f) => f.toLowerCase() === domain.toLowerCase());
@@ -3785,8 +3827,8 @@ async function fetchContexts(domains, token, repo, targetDir) {
3785
3827
  result.failureReasons[domain] = "Not found in archive";
3786
3828
  continue;
3787
3829
  }
3788
- const domainPath = (0, import_path2.join)(contextsDir, domain);
3789
- copyDirectory((0, import_path2.join)(tempDir, match), domainPath);
3830
+ const domainPath = (0, import_path4.join)(contextsDir, domain);
3831
+ copyDirectory((0, import_path4.join)(archive.extractedRoot, match), domainPath);
3790
3832
  result.successful.push(domain);
3791
3833
  } catch (error) {
3792
3834
  result.failed.push(domain);
@@ -3794,59 +3836,51 @@ async function fetchContexts(domains, token, repo, targetDir) {
3794
3836
  }
3795
3837
  }
3796
3838
  } finally {
3797
- if ((0, import_fs2.existsSync)(tempDir)) {
3798
- try {
3799
- (0, import_fs2.rmSync)(tempDir, { recursive: true, force: true });
3800
- } catch {
3801
- }
3802
- }
3839
+ archive.cleanup();
3803
3840
  }
3804
3841
  return result;
3805
3842
  }
3806
- function copyDirectory(source, target) {
3807
- if (!(0, import_fs2.existsSync)(target)) (0, import_fs2.mkdirSync)(target, { recursive: true });
3808
- const entries = (0, import_fs2.readdirSync)(source, { withFileTypes: true });
3809
- for (const entry of entries) {
3810
- const sourcePath = (0, import_path2.join)(source, entry.name);
3811
- const targetPath = (0, import_path2.join)(target, entry.name);
3812
- if (entry.isDirectory()) {
3813
- copyDirectory(sourcePath, targetPath);
3814
- } else {
3815
- (0, import_fs2.copyFileSync)(sourcePath, targetPath);
3816
- }
3817
- }
3818
- }
3819
- function getReadableError(status) {
3820
- switch (status) {
3821
- case 401:
3822
- return "Authentication failed. Check your GitHub token.";
3823
- case 403:
3824
- return "Access denied. Verify token permissions and rate limits.";
3825
- case 404:
3826
- return "Resource not found. Verify the repository and release exist.";
3827
- case 429:
3828
- return "Rate limit exceeded. Please wait and try again.";
3829
- case 500:
3830
- case 502:
3831
- case 503:
3832
- return "GitHub server error. Please try again later.";
3833
- default:
3834
- return `Unexpected error (${status})`;
3835
- }
3836
- }
3837
3843
 
3838
3844
  // src/steps/resources/fetch-factory.ts
3839
- var import_fs4 = require("fs");
3840
- var import_path4 = require("path");
3841
- var import_child_process3 = require("child_process");
3845
+ var import_fs7 = require("fs");
3846
+ var import_path7 = require("path");
3842
3847
 
3843
3848
  // src/bundles/run-bundle.ts
3844
- var import_fs3 = require("fs");
3845
- var import_path3 = require("path");
3849
+ var import_fs6 = require("fs");
3850
+ var import_path6 = require("path");
3846
3851
 
3847
3852
  // src/bundles/types.ts
3848
3853
  var SCHEMA_VERSION = 1;
3849
3854
 
3855
+ // src/utils/marker-block.ts
3856
+ var import_fs5 = require("fs");
3857
+ var import_path5 = require("path");
3858
+ function upsertMarkerBlock(filePath, startMarker, endMarker, body, opts = {}) {
3859
+ const block = `${startMarker}
3860
+ ${body}
3861
+ ${endMarker}`;
3862
+ (0, import_fs5.mkdirSync)((0, import_path5.dirname)(filePath), { recursive: true });
3863
+ if (!(0, import_fs5.existsSync)(filePath)) {
3864
+ (0, import_fs5.writeFileSync)(filePath, block + "\n", "utf-8");
3865
+ return;
3866
+ }
3867
+ const existing = (0, import_fs5.readFileSync)(filePath, "utf-8");
3868
+ const s = existing.indexOf(startMarker);
3869
+ const e = existing.indexOf(endMarker);
3870
+ const hasS = s !== -1;
3871
+ const hasE = e !== -1;
3872
+ if (hasS && !hasE || !hasS && hasE || hasS && hasE && e < s) {
3873
+ opts.onCorrupt?.(filePath, startMarker, endMarker);
3874
+ }
3875
+ if (hasS && hasE && e > s) {
3876
+ const updated = existing.slice(0, s) + block + existing.slice(e + endMarker.length);
3877
+ (0, import_fs5.writeFileSync)(filePath, updated, "utf-8");
3878
+ } else {
3879
+ const sep3 = existing.endsWith("\n") ? "\n" : "\n\n";
3880
+ (0, import_fs5.writeFileSync)(filePath, existing + sep3 + block + "\n", "utf-8");
3881
+ }
3882
+ }
3883
+
3850
3884
  // src/bundles/run-bundle.ts
3851
3885
  var OWNER_KEY = "_bundleOwner";
3852
3886
  async function runBundle(manifest, ctx) {
@@ -3864,7 +3898,7 @@ async function runBundle(manifest, ctx) {
3864
3898
  }
3865
3899
  if (manifest.gitignore?.length) {
3866
3900
  upsertMarkerBlock(
3867
- (0, import_path3.join)(ctx.projectPath, ".gitignore"),
3901
+ (0, import_path6.join)(ctx.projectPath, ".gitignore"),
3868
3902
  `# ${manifest.name}:start`,
3869
3903
  `# ${manifest.name}:end`,
3870
3904
  manifest.gitignore.join("\n")
@@ -3876,7 +3910,7 @@ async function runBundle(manifest, ctx) {
3876
3910
  result.instructionsSnippet = snippet;
3877
3911
  if (!ctx.skipInstructions) {
3878
3912
  upsertMarkerBlock(
3879
- (0, import_path3.join)(ctx.projectPath, ctx.instructionsFile),
3913
+ (0, import_path6.join)(ctx.projectPath, ctx.instructionsFile),
3880
3914
  `<!-- ${manifest.name}:start -->`,
3881
3915
  `<!-- ${manifest.name}:end -->`,
3882
3916
  snippet
@@ -3903,12 +3937,12 @@ function executeOp(op, owner, ctx, result) {
3903
3937
  function runCopy(op, ctx, result) {
3904
3938
  const source = resolveBundlePath(op.from, ctx);
3905
3939
  const target = resolveProjectPath(op.to, ctx);
3906
- if (!(0, import_fs3.existsSync)(source)) return;
3907
- if ((0, import_fs3.statSync)(source).isDirectory()) {
3908
- copyDirectory2(source, target);
3940
+ if (!(0, import_fs6.existsSync)(source)) return;
3941
+ if ((0, import_fs6.statSync)(source).isDirectory()) {
3942
+ copyDirectory(source, target);
3909
3943
  } else {
3910
- (0, import_fs3.mkdirSync)((0, import_path3.dirname)(target), { recursive: true });
3911
- (0, import_fs3.copyFileSync)(source, target);
3944
+ (0, import_fs6.mkdirSync)((0, import_path6.dirname)(target), { recursive: true });
3945
+ (0, import_fs6.copyFileSync)(source, target);
3912
3946
  }
3913
3947
  result.filesTouched.push(op.to);
3914
3948
  }
@@ -3918,14 +3952,14 @@ function runMergeJson(op, owner, ctx, result) {
3918
3952
  const stripped = stripOwnedEntries(existing, owner);
3919
3953
  const tagged = tagEntries(op.patch, owner);
3920
3954
  const merged = deepMerge2(stripped, tagged);
3921
- (0, import_fs3.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
3922
- (0, import_fs3.writeFileSync)(filePath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
3955
+ (0, import_fs6.mkdirSync)((0, import_path6.dirname)(filePath), { recursive: true });
3956
+ (0, import_fs6.writeFileSync)(filePath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
3923
3957
  result.filesTouched.push(op.file);
3924
3958
  }
3925
3959
  function readJsonOrEmpty(path) {
3926
- if (!(0, import_fs3.existsSync)(path)) return {};
3960
+ if (!(0, import_fs6.existsSync)(path)) return {};
3927
3961
  try {
3928
- return JSON.parse((0, import_fs3.readFileSync)(path, "utf-8"));
3962
+ return JSON.parse((0, import_fs6.readFileSync)(path, "utf-8"));
3929
3963
  } catch {
3930
3964
  return {};
3931
3965
  }
@@ -3970,38 +4004,18 @@ function deepMerge2(a, b) {
3970
4004
  }
3971
4005
  return b;
3972
4006
  }
3973
- function upsertMarkerBlock(filePath, startMarker, endMarker, body) {
3974
- const block = `${startMarker}
3975
- ${body}
3976
- ${endMarker}`;
3977
- (0, import_fs3.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
3978
- if (!(0, import_fs3.existsSync)(filePath)) {
3979
- (0, import_fs3.writeFileSync)(filePath, block + "\n", "utf-8");
3980
- return;
3981
- }
3982
- const existing = (0, import_fs3.readFileSync)(filePath, "utf-8");
3983
- const s = existing.indexOf(startMarker);
3984
- const e = existing.indexOf(endMarker);
3985
- if (s !== -1 && e !== -1 && e > s) {
3986
- const updated = existing.slice(0, s) + block + existing.slice(e + endMarker.length);
3987
- (0, import_fs3.writeFileSync)(filePath, updated, "utf-8");
3988
- } else {
3989
- const joiner = existing.endsWith("\n") ? "\n" : "\n\n";
3990
- (0, import_fs3.writeFileSync)(filePath, existing + joiner + block + "\n", "utf-8");
3991
- }
3992
- }
3993
4007
  function resolveBundlePath(rel, ctx) {
3994
- const base = (0, import_path3.resolve)(ctx.bundleRoot);
3995
- const full = (0, import_path3.resolve)((0, import_path3.join)(base, rel));
3996
- if (full !== base && !full.startsWith(base + import_path3.sep)) {
4008
+ const base = (0, import_path6.resolve)(ctx.bundleRoot);
4009
+ const full = (0, import_path6.resolve)((0, import_path6.join)(base, rel));
4010
+ if (full !== base && !full.startsWith(base + import_path6.sep)) {
3997
4011
  throw new Error(`Bundle op "from" escapes bundle root: ${rel}`);
3998
4012
  }
3999
4013
  return full;
4000
4014
  }
4001
4015
  function resolveProjectPath(rel, ctx) {
4002
- const base = (0, import_path3.resolve)(ctx.projectPath);
4003
- const full = (0, import_path3.resolve)((0, import_path3.join)(base, rel));
4004
- if (full !== base && !full.startsWith(base + import_path3.sep)) {
4016
+ const base = (0, import_path6.resolve)(ctx.projectPath);
4017
+ const full = (0, import_path6.resolve)((0, import_path6.join)(base, rel));
4018
+ if (full !== base && !full.startsWith(base + import_path6.sep)) {
4005
4019
  throw new Error(`Bundle op "to" escapes project path: ${rel}`);
4006
4020
  }
4007
4021
  return full;
@@ -4019,22 +4033,12 @@ function assertSchemaCompatible(manifest) {
4019
4033
  }
4020
4034
  }
4021
4035
  function assertBundleRootExists(ctx) {
4022
- if (!(0, import_fs3.existsSync)(ctx.bundleRoot) || !(0, import_fs3.statSync)(ctx.bundleRoot).isDirectory()) {
4036
+ if (!(0, import_fs6.existsSync)(ctx.bundleRoot) || !(0, import_fs6.statSync)(ctx.bundleRoot).isDirectory()) {
4023
4037
  throw new Error(`Bundle root not found or not a directory: ${ctx.bundleRoot}`);
4024
4038
  }
4025
4039
  }
4026
- function copyDirectory2(source, target) {
4027
- if (!(0, import_fs3.existsSync)(target)) (0, import_fs3.mkdirSync)(target, { recursive: true });
4028
- for (const entry of (0, import_fs3.readdirSync)(source, { withFileTypes: true })) {
4029
- const s = (0, import_path3.join)(source, entry.name);
4030
- const t = (0, import_path3.join)(target, entry.name);
4031
- if (entry.isDirectory()) copyDirectory2(s, t);
4032
- else (0, import_fs3.copyFileSync)(s, t);
4033
- }
4034
- }
4035
4040
 
4036
4041
  // src/steps/resources/fetch-factory.ts
4037
- var MIN_FILE_SIZE2 = 1024;
4038
4042
  function getFactoryAsset(agent) {
4039
4043
  switch (agent) {
4040
4044
  case "github-copilot":
@@ -4050,114 +4054,58 @@ var fetch_factory_default = defineStep({
4050
4054
  execute: async (ctx) => {
4051
4055
  const result = await fetchFactory(ctx.token, ctx.factoryRepo, ctx.config.projectPath, ctx.config.agent);
4052
4056
  ctx.installed.factoryInstalled = result.success;
4053
- ctx.installed.factoryBundleManaged = result.bundleManaged ?? false;
4054
4057
  if (result.instructionsSnippet) {
4055
- const stash = ctx.installed.bundleInstructions ?? {};
4056
- stash["factory"] = result.instructionsSnippet;
4057
- ctx.installed.bundleInstructions = stash;
4058
+ ctx.installed.factoryInstructionsSnippet = result.instructionsSnippet;
4058
4059
  }
4059
4060
  if (!result.success) {
4060
4061
  return { status: "failed", detail: result.failureReason };
4061
4062
  }
4062
- const tag = result.bundleManaged ? " (bundle)" : "";
4063
- return { status: "success", message: result.filesInstalled.join(", ") + tag };
4063
+ return { status: "success", message: result.filesInstalled.join(", ") };
4064
4064
  }
4065
4065
  });
4066
4066
  async function fetchFactory(token, repo, targetDir, agent) {
4067
4067
  const result = { success: false, filesInstalled: [] };
4068
- const tarCheck = (0, import_child_process3.spawnSync)("tar", ["--version"], { stdio: "ignore" });
4069
- if (tarCheck.status !== 0) {
4070
- result.failureReason = "tar command not found";
4071
- return result;
4072
- }
4073
4068
  const { assetName, rootFolder } = getFactoryAsset(agent);
4074
- const headers = {
4075
- "Accept": "application/vnd.github+json",
4076
- "Authorization": `Bearer ${token}`
4077
- };
4078
- const releaseResponse = await fetchWithRetry(
4079
- `https://api.github.com/repos/${repo}/releases/latest`,
4080
- { headers }
4081
- );
4082
- if (!releaseResponse.ok) {
4083
- result.failureReason = `GitHub API error (${releaseResponse.status})`;
4069
+ let release;
4070
+ try {
4071
+ release = await fetchLatestRelease(token, repo);
4072
+ } catch (err) {
4073
+ result.failureReason = err instanceof Error ? err.message : String(err);
4084
4074
  return result;
4085
4075
  }
4086
- const releaseData = await releaseResponse.json();
4087
- const asset = releaseData.assets?.find((a) => a.name === assetName);
4076
+ const asset = release.assets.find((a) => a.name === assetName);
4088
4077
  if (!asset) {
4089
4078
  result.failureReason = `${assetName} not found in release`;
4090
4079
  return result;
4091
4080
  }
4092
- const tempDir = (0, import_path4.join)(targetDir, ".temp-factory-download");
4081
+ let archive = null;
4093
4082
  try {
4094
- if ((0, import_fs4.existsSync)(tempDir)) (0, import_fs4.rmSync)(tempDir, { recursive: true, force: true });
4095
- (0, import_fs4.mkdirSync)(tempDir, { recursive: true });
4096
- const response = await fetchWithRetry(asset.url, {
4097
- headers: { "Accept": "application/octet-stream", "Authorization": `Bearer ${token}` }
4098
- });
4099
- if (!response.ok) {
4100
- result.failureReason = `Download failed (${response.status})`;
4101
- return result;
4102
- }
4103
- const archivePath = (0, import_path4.join)(tempDir, assetName);
4104
- const arrayBuffer = await response.arrayBuffer();
4105
- (0, import_fs4.writeFileSync)(archivePath, Buffer.from(arrayBuffer));
4106
- const stats = (0, import_fs4.statSync)(archivePath);
4107
- if (stats.size < MIN_FILE_SIZE2) {
4108
- result.failureReason = "Downloaded file corrupted (too small)";
4109
- return result;
4110
- }
4111
- const listResult = (0, import_child_process3.spawnSync)("tar", ["-tzf", archivePath], { encoding: "utf8" });
4112
- if (listResult.status !== 0) {
4113
- result.failureReason = "Failed to read archive contents";
4114
- return result;
4115
- }
4116
- const resolvedTempDir = (0, import_path4.resolve)(tempDir);
4117
- for (const entry of listResult.stdout.split("\n").filter(Boolean)) {
4118
- const entryPath = (0, import_path4.resolve)((0, import_path4.join)(tempDir, entry.replace(/\/$/, "")));
4119
- if (!entryPath.startsWith(resolvedTempDir + import_path4.sep)) {
4120
- result.failureReason = `Archive contains unsafe path: ${entry}`;
4121
- return result;
4122
- }
4123
- }
4124
- const extractResult = (0, import_child_process3.spawnSync)("tar", ["-xzf", archivePath, "-C", tempDir], { stdio: "ignore" });
4125
- if (extractResult.status !== 0) {
4126
- result.failureReason = "Archive extraction failed";
4127
- return result;
4128
- }
4129
- const extractedEntries = (0, import_fs4.readdirSync)(tempDir);
4083
+ archive = await downloadAndExtractAsset(token, asset, targetDir, ".temp-factory-download");
4084
+ const extractedEntries = (0, import_fs7.readdirSync)(archive.extractedRoot);
4130
4085
  const extractedFolder = extractedEntries.find((e) => e.toLowerCase() === rootFolder.toLowerCase());
4131
4086
  if (!extractedFolder) {
4132
4087
  result.failureReason = `No ${rootFolder}/ folder in archive`;
4133
4088
  return result;
4134
4089
  }
4135
- const extractedPath = (0, import_path4.join)(tempDir, extractedFolder);
4136
- const manifestPath = (0, import_path4.join)(extractedPath, "install.json");
4137
- if ((0, import_fs4.existsSync)(manifestPath)) {
4138
- await installViaBundle(manifestPath, extractedPath, targetDir, agent, result);
4139
- } else if (agent === "github-copilot") {
4140
- installCopilotFactory(extractedPath, targetDir, result);
4141
- } else {
4142
- installClaudeFactory(extractedPath, targetDir, result);
4090
+ const extractedPath = (0, import_path7.join)(archive.extractedRoot, extractedFolder);
4091
+ const manifestPath = (0, import_path7.join)(extractedPath, "install.json");
4092
+ if (!(0, import_fs7.existsSync)(manifestPath)) {
4093
+ result.failureReason = "Factory archive has no install.json \u2014 upgrade Factory or downgrade installer";
4094
+ return result;
4143
4095
  }
4096
+ await installViaBundle(manifestPath, extractedPath, targetDir, agent, result);
4144
4097
  result.success = true;
4145
4098
  } catch (error) {
4146
4099
  result.failureReason = error instanceof Error ? error.message : String(error);
4147
4100
  } finally {
4148
- if ((0, import_fs4.existsSync)(tempDir)) {
4149
- try {
4150
- (0, import_fs4.rmSync)(tempDir, { recursive: true, force: true });
4151
- } catch {
4152
- }
4153
- }
4101
+ archive?.cleanup();
4154
4102
  }
4155
4103
  return result;
4156
4104
  }
4157
4105
  async function installViaBundle(manifestPath, extractedPath, targetDir, agent, result) {
4158
4106
  let manifest;
4159
4107
  try {
4160
- manifest = JSON.parse((0, import_fs4.readFileSync)(manifestPath, "utf-8"));
4108
+ manifest = JSON.parse((0, import_fs7.readFileSync)(manifestPath, "utf-8"));
4161
4109
  } catch (e) {
4162
4110
  throw new Error(`install.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
4163
4111
  }
@@ -4170,145 +4118,18 @@ async function installViaBundle(manifestPath, extractedPath, targetDir, agent, r
4170
4118
  skipInstructions: true
4171
4119
  // write-instructions.ts commits the snippet for correct ordering
4172
4120
  });
4173
- result.bundleManaged = true;
4174
4121
  result.instructionsSnippet = runResult.instructionsSnippet;
4175
4122
  result.filesInstalled = runResult.filesTouched;
4176
4123
  }
4177
- function installClaudeFactory(extractedPath, targetDir, result) {
4178
- const claudeDir = (0, import_path4.join)(targetDir, ".claude");
4179
- const factoryDir = (0, import_path4.join)(targetDir, "factory");
4180
- const agentsSrc = (0, import_path4.join)(extractedPath, "agents");
4181
- if ((0, import_fs4.existsSync)(agentsSrc)) {
4182
- copyDirectory3(agentsSrc, (0, import_path4.join)(claudeDir, "agents"));
4183
- result.filesInstalled.push(".claude/agents/");
4184
- }
4185
- const hooksSrc = (0, import_path4.join)(extractedPath, "hooks");
4186
- if ((0, import_fs4.existsSync)(hooksSrc)) {
4187
- copyDirectory3(hooksSrc, (0, import_path4.join)(claudeDir, "hooks"));
4188
- result.filesInstalled.push(".claude/hooks/");
4189
- }
4190
- const skillsSrc = (0, import_path4.join)(extractedPath, "skills");
4191
- if ((0, import_fs4.existsSync)(skillsSrc)) {
4192
- copyDirectory3(skillsSrc, (0, import_path4.join)(claudeDir, "skills"));
4193
- result.filesInstalled.push(".claude/skills/");
4194
- }
4195
- const workflowSrc = (0, import_path4.join)(extractedPath, "workflow");
4196
- if ((0, import_fs4.existsSync)(workflowSrc)) {
4197
- copyDirectory3(workflowSrc, (0, import_path4.join)(factoryDir, "workflow"));
4198
- result.filesInstalled.push("factory/workflow/");
4199
- }
4200
- const utilsSrc = (0, import_path4.join)(extractedPath, "utils");
4201
- if ((0, import_fs4.existsSync)(utilsSrc)) {
4202
- copyDirectory3(utilsSrc, (0, import_path4.join)(factoryDir, "utils"));
4203
- result.filesInstalled.push("factory/utils/");
4204
- }
4205
- const docsSrc = (0, import_path4.join)(extractedPath, "docs");
4206
- if ((0, import_fs4.existsSync)(docsSrc)) {
4207
- copyDirectory3(docsSrc, (0, import_path4.join)(factoryDir, "docs"));
4208
- result.filesInstalled.push("factory/docs/");
4209
- }
4210
- const rootEntries = (0, import_fs4.readdirSync)(extractedPath, { withFileTypes: true });
4211
- for (const entry of rootEntries) {
4212
- if (entry.isFile()) {
4213
- if (!(0, import_fs4.existsSync)(factoryDir)) (0, import_fs4.mkdirSync)(factoryDir, { recursive: true });
4214
- (0, import_fs4.copyFileSync)((0, import_path4.join)(extractedPath, entry.name), (0, import_path4.join)(factoryDir, entry.name));
4215
- result.filesInstalled.push(`factory/${entry.name}`);
4216
- }
4217
- }
4218
- configureSettings(claudeDir);
4219
- result.filesInstalled.push(".claude/settings.json");
4220
- }
4221
- function installCopilotFactory(extractedPath, targetDir, result) {
4222
- const factoryDir = (0, import_path4.join)(targetDir, "factory");
4223
- copyDirectory3(extractedPath, factoryDir);
4224
- const entries = (0, import_fs4.readdirSync)(extractedPath, { withFileTypes: true });
4225
- for (const entry of entries) {
4226
- if (entry.isDirectory()) {
4227
- result.filesInstalled.push(`factory/${entry.name}/`);
4228
- } else {
4229
- result.filesInstalled.push(`factory/${entry.name}`);
4230
- }
4231
- }
4232
- }
4233
- function configureSettings(claudeDir) {
4234
- const settingsPath = (0, import_path4.join)(claudeDir, "settings.json");
4235
- let settings = {};
4236
- if ((0, import_fs4.existsSync)(settingsPath)) {
4237
- try {
4238
- settings = JSON.parse((0, import_fs4.readFileSync)(settingsPath, "utf8"));
4239
- } catch {
4240
- }
4241
- }
4242
- if (!settings.hooks) settings.hooks = {};
4243
- const welcomeHook = {
4244
- hooks: [{ type: "command", command: "node .claude/hooks/factory-welcome.cjs" }]
4245
- };
4246
- const immutableGuardHook = {
4247
- matcher: "Write|Edit",
4248
- hooks: [{ type: "command", command: "node .claude/hooks/factory-immutable-guard.cjs" }]
4249
- };
4250
- const stateAdvanceHook = {
4251
- matcher: "Write|Edit",
4252
- hooks: [{ type: "command", command: "node .claude/hooks/factory-state-advance.cjs" }]
4253
- };
4254
- const contextMonitorHook = {
4255
- hooks: [{ type: "command", command: "node .claude/hooks/factory-context-monitor.cjs" }]
4256
- };
4257
- const stateGuardHook = {
4258
- hooks: [{ type: "command", command: "node .claude/hooks/factory-state-guard.cjs" }]
4259
- };
4260
- for (const event of ["SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit"]) {
4261
- if (settings.hooks[event]) {
4262
- settings.hooks[event] = settings.hooks[event].filter(
4263
- (h) => !h.hooks?.some(
4264
- (hh) => hh.command?.includes("factory-welcome") || hh.command?.includes("factory-immutable-guard") || hh.command?.includes("factory-state-advance") || hh.command?.includes("factory-context-monitor") || hh.command?.includes("factory-state-guard")
4265
- )
4266
- );
4267
- if (settings.hooks[event].length === 0) delete settings.hooks[event];
4268
- }
4269
- }
4270
- if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
4271
- settings.hooks.SessionStart.unshift(welcomeHook);
4272
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
4273
- settings.hooks.PreToolUse.unshift(immutableGuardHook);
4274
- if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
4275
- settings.hooks.PostToolUse.unshift(stateAdvanceHook);
4276
- if (!settings.hooks.UserPromptSubmit) {
4277
- settings.hooks.UserPromptSubmit = [stateGuardHook, contextMonitorHook];
4278
- } else {
4279
- const hasGuard = settings.hooks.UserPromptSubmit.some(
4280
- (h) => h.hooks?.some((hh) => hh.command?.includes("factory-state-guard"))
4281
- );
4282
- if (!hasGuard) settings.hooks.UserPromptSubmit.unshift(stateGuardHook);
4283
- const hasMonitor = settings.hooks.UserPromptSubmit.some(
4284
- (h) => h.hooks?.some((hh) => hh.command?.includes("factory-context-monitor"))
4285
- );
4286
- if (!hasMonitor) settings.hooks.UserPromptSubmit.push(contextMonitorHook);
4287
- }
4288
- settings.statusLine = { type: "command", command: "node .claude/hooks/factory-statusline.cjs" };
4289
- if (!(0, import_fs4.existsSync)(claudeDir)) (0, import_fs4.mkdirSync)(claudeDir, { recursive: true });
4290
- (0, import_fs4.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4291
- }
4292
- function copyDirectory3(source, target) {
4293
- if (!(0, import_fs4.existsSync)(target)) (0, import_fs4.mkdirSync)(target, { recursive: true });
4294
- const entries = (0, import_fs4.readdirSync)(source, { withFileTypes: true });
4295
- for (const entry of entries) {
4296
- const sourcePath = (0, import_path4.join)(source, entry.name);
4297
- const targetPath = (0, import_path4.join)(target, entry.name);
4298
- if (entry.isDirectory()) {
4299
- copyDirectory3(sourcePath, targetPath);
4300
- } else {
4301
- (0, import_fs4.copyFileSync)(sourcePath, targetPath);
4302
- }
4303
- }
4304
- }
4305
4124
 
4306
4125
  // src/steps/setup/write-instructions.ts
4307
- var import_fs5 = require("fs");
4308
- var import_path5 = require("path");
4126
+ var import_fs8 = require("fs");
4127
+ var import_path8 = require("path");
4309
4128
 
4310
4129
  // src/steps/shared.ts
4130
+ var cached = null;
4311
4131
  function getFilteredMcpConfig(ctx) {
4132
+ if (cached) return cached;
4312
4133
  const successfulDomains = new Set(ctx.installed.domainsInstalled ?? []);
4313
4134
  const successfulTechnologies = ctx.config.technologies.filter(
4314
4135
  (p) => p.domains.every((d) => successfulDomains.has(d.toLowerCase()))
@@ -4317,9 +4138,10 @@ function getFilteredMcpConfig(ctx) {
4317
4138
  ...ctx.config.baseMcpServers,
4318
4139
  ...successfulTechnologies.flatMap((p) => p.mcpServers)
4319
4140
  ]);
4320
- return Object.fromEntries(
4141
+ cached = Object.fromEntries(
4321
4142
  Object.entries(ctx.config.mcpConfig).filter(([name]) => successfulMcpServers.has(name))
4322
4143
  );
4144
+ return cached;
4323
4145
  }
4324
4146
 
4325
4147
  // src/steps/setup/write-instructions.ts
@@ -4329,107 +4151,67 @@ var red = (text) => `\x1B[31m${text}\x1B[0m`;
4329
4151
  var write_instructions_default = defineStep({
4330
4152
  name: "write-instructions",
4331
4153
  label: "Writing instruction file",
4332
- when: (ctx) => (ctx.installed.domainsInstalled?.length ?? 0) > 0 || Object.keys(ctx.config.mcpConfig).length > 0 || ctx.installed.factoryInstalled || Object.keys(ctx.installed.bundleInstructions ?? {}).length > 0,
4154
+ when: (ctx) => (ctx.installed.domainsInstalled?.length ?? 0) > 0 || Object.keys(ctx.config.mcpConfig).length > 0 || !!ctx.installed.factoryInstructionsSnippet,
4333
4155
  execute: async (ctx) => {
4334
4156
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4335
4157
  const domains = ctx.installed.domainsInstalled ?? [];
4336
4158
  const agent = ctx.config.agent;
4337
4159
  const projectPath = ctx.config.projectPath;
4338
- const factoryInstalled = ctx.installed.factoryInstalled ?? false;
4339
- const factoryBundleManaged = ctx.installed.factoryBundleManaged ?? false;
4340
- const includeFactorySection = factoryInstalled && !factoryBundleManaged;
4341
- const content = buildCombinedInstructions(domains, filteredMcpConfig, agent, projectPath, includeFactorySection);
4160
+ const content = buildCombinedInstructions(domains, filteredMcpConfig, agent, projectPath);
4342
4161
  const target = getAgentTarget(agent);
4343
- const filePath = (0, import_path5.join)(projectPath, target.instructions);
4344
- const fileDir = (0, import_path5.dirname)(filePath);
4345
- if (!(0, import_fs5.existsSync)(fileDir)) (0, import_fs5.mkdirSync)(fileDir, { recursive: true });
4346
- if (agent === "cursor" && !(0, import_fs5.existsSync)(filePath)) {
4347
- (0, import_fs5.writeFileSync)(filePath, `---
4162
+ const filePath = (0, import_path8.join)(projectPath, target.instructions);
4163
+ const fileDir = (0, import_path8.dirname)(filePath);
4164
+ if (!(0, import_fs8.existsSync)(fileDir)) (0, import_fs8.mkdirSync)(fileDir, { recursive: true });
4165
+ if (agent === "cursor" && !(0, import_fs8.existsSync)(filePath)) {
4166
+ (0, import_fs8.writeFileSync)(filePath, `---
4348
4167
  description: AI development instructions from One-Shot Installer
4349
4168
  alwaysApply: true
4350
4169
  ---
4351
4170
 
4352
4171
  `, "utf-8");
4353
4172
  }
4354
- upsertBlock(filePath, content);
4355
- const bundleSnippets = ctx.installed.bundleInstructions ?? {};
4356
- for (const bundleName of Object.keys(bundleSnippets).sort()) {
4357
- upsertBundleBlock(filePath, bundleName, bundleSnippets[bundleName]);
4173
+ upsertMarkerBlock(filePath, MARKER_START, MARKER_END, content, {
4174
+ onCorrupt: (file) => {
4175
+ const fileName = file.split(/[/\\]/).pop() ?? file;
4176
+ const box = [
4177
+ "",
4178
+ red(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
4179
+ red(" \u2551 \u26A0 INSTRUCTION FILE COULD NOT BE UPDATED \u2551"),
4180
+ red(" \u2551 \u2551"),
4181
+ red(` \u2551 ${fileName} has corrupted markers.`.padEnd(52) + "\u2551"),
4182
+ red(" \u2551 Fix: delete the lines containing \u2551"),
4183
+ red(" \u2551 <!-- one-shot-installer:start --> \u2551"),
4184
+ red(" \u2551 <!-- one-shot-installer:end --> \u2551"),
4185
+ red(" \u2551 Then re-run the installer. \u2551"),
4186
+ red(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
4187
+ ""
4188
+ ].join("\n");
4189
+ console.log(box);
4190
+ throw new Error(`Corrupted markers in ${fileName} \u2014 fix manually and re-run`);
4191
+ }
4192
+ });
4193
+ if (ctx.installed.factoryInstructionsSnippet) {
4194
+ upsertMarkerBlock(
4195
+ filePath,
4196
+ "<!-- factory:start -->",
4197
+ "<!-- factory:end -->",
4198
+ ctx.installed.factoryInstructionsSnippet
4199
+ );
4358
4200
  }
4359
4201
  return { status: "success", message: target.instructions };
4360
4202
  }
4361
4203
  });
4362
- function upsertBlock(filePath, block) {
4363
- const marked = `${MARKER_START}
4364
- ${block}
4365
- ${MARKER_END}`;
4366
- if (!(0, import_fs5.existsSync)(filePath)) {
4367
- (0, import_fs5.writeFileSync)(filePath, marked, "utf-8");
4368
- return;
4369
- }
4370
- const existing = (0, import_fs5.readFileSync)(filePath, "utf-8");
4371
- const start = existing.indexOf(MARKER_START);
4372
- const end = existing.indexOf(MARKER_END);
4373
- const hasStart = start !== -1;
4374
- const hasEnd = end !== -1;
4375
- if (hasStart && !hasEnd || !hasStart && hasEnd || hasStart && hasEnd && end < start) {
4376
- const fileName = filePath.split(/[/\\]/).pop() ?? filePath;
4377
- const box = [
4378
- "",
4379
- red(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
4380
- red(" \u2551 \u26A0 INSTRUCTION FILE COULD NOT BE UPDATED \u2551"),
4381
- red(" \u2551 \u2551"),
4382
- red(` \u2551 ${fileName} has corrupted markers.`.padEnd(52) + "\u2551"),
4383
- red(" \u2551 Fix: delete the lines containing \u2551"),
4384
- red(" \u2551 <!-- one-shot-installer:start --> \u2551"),
4385
- red(" \u2551 <!-- one-shot-installer:end --> \u2551"),
4386
- red(" \u2551 Then re-run the installer. \u2551"),
4387
- red(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
4388
- ""
4389
- ].join("\n");
4390
- console.log(box);
4391
- throw new Error(`Corrupted markers in ${fileName} \u2014 fix manually and re-run`);
4392
- }
4393
- if (hasStart && hasEnd) {
4394
- const updated = existing.slice(0, start) + marked + existing.slice(end + MARKER_END.length);
4395
- (0, import_fs5.writeFileSync)(filePath, updated, "utf-8");
4396
- } else {
4397
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
4398
- (0, import_fs5.writeFileSync)(filePath, existing + separator + marked, "utf-8");
4399
- }
4400
- }
4401
- function upsertBundleBlock(filePath, bundleName, snippet) {
4402
- const startMarker = `<!-- ${bundleName}:start -->`;
4403
- const endMarker = `<!-- ${bundleName}:end -->`;
4404
- const block = `${startMarker}
4405
- ${snippet}
4406
- ${endMarker}`;
4407
- if (!(0, import_fs5.existsSync)(filePath)) {
4408
- (0, import_fs5.writeFileSync)(filePath, block + "\n", "utf-8");
4409
- return;
4410
- }
4411
- const existing = (0, import_fs5.readFileSync)(filePath, "utf-8");
4412
- const s = existing.indexOf(startMarker);
4413
- const e = existing.indexOf(endMarker);
4414
- if (s !== -1 && e !== -1 && e > s) {
4415
- const updated = existing.slice(0, s) + block + existing.slice(e + endMarker.length);
4416
- (0, import_fs5.writeFileSync)(filePath, updated, "utf-8");
4417
- } else {
4418
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
4419
- (0, import_fs5.writeFileSync)(filePath, existing + separator + block + "\n", "utf-8");
4420
- }
4421
- }
4422
4204
  function collectMdFiles(dir) {
4423
- if (!(0, import_fs5.existsSync)(dir)) return [];
4424
- const entries = (0, import_fs5.readdirSync)(dir, { recursive: true });
4205
+ if (!(0, import_fs8.existsSync)(dir)) return [];
4206
+ const entries = (0, import_fs8.readdirSync)(dir, { recursive: true });
4425
4207
  return entries.filter((entry) => {
4426
- const fullPath = (0, import_path5.join)(dir, entry);
4427
- return entry.endsWith(".md") && (0, import_fs5.statSync)(fullPath).isFile();
4208
+ const fullPath = (0, import_path8.join)(dir, entry);
4209
+ return entry.endsWith(".md") && (0, import_fs8.statSync)(fullPath).isFile();
4428
4210
  }).map((entry) => entry.replace(/\\/g, "/")).sort();
4429
4211
  }
4430
4212
  function extractFirstHeading(filePath) {
4431
4213
  try {
4432
- const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
4214
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
4433
4215
  const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, "");
4434
4216
  const match = withoutFrontmatter.match(/^#\s+(.+)/m);
4435
4217
  if (match) {
@@ -4447,16 +4229,16 @@ function formatPathRef(contextPath, description, agent) {
4447
4229
  return `- \`${contextPath}\` \u2014 ${description}`;
4448
4230
  }
4449
4231
  function resolveDomainFolder(domain, contextsDir) {
4450
- if ((0, import_fs5.existsSync)(contextsDir)) {
4232
+ if ((0, import_fs8.existsSync)(contextsDir)) {
4451
4233
  try {
4452
- const entries = (0, import_fs5.readdirSync)(contextsDir);
4234
+ const entries = (0, import_fs8.readdirSync)(contextsDir);
4453
4235
  const match = entries.find((e) => e.toLowerCase() === domain.toLowerCase());
4454
- if (match) return { folderName: match, folderPath: (0, import_path5.join)(contextsDir, match) };
4236
+ if (match) return { folderName: match, folderPath: (0, import_path8.join)(contextsDir, match) };
4455
4237
  } catch {
4456
4238
  }
4457
4239
  }
4458
4240
  const fallback = domain.toUpperCase();
4459
- return { folderName: fallback, folderPath: (0, import_path5.join)(contextsDir, fallback) };
4241
+ return { folderName: fallback, folderPath: (0, import_path8.join)(contextsDir, fallback) };
4460
4242
  }
4461
4243
  function buildContextRefsSection(domains, agent, contextsDir) {
4462
4244
  const lines2 = [
@@ -4468,34 +4250,34 @@ function buildContextRefsSection(domains, agent, contextsDir) {
4468
4250
  let hasAnyFiles = false;
4469
4251
  for (const domain of domains) {
4470
4252
  const { folderName, folderPath: domainPath } = resolveDomainFolder(domain, contextsDir);
4471
- if (!(0, import_fs5.existsSync)(domainPath)) continue;
4253
+ if (!(0, import_fs8.existsSync)(domainPath)) continue;
4472
4254
  const domainFiles = [];
4473
- const ctxInstructions = (0, import_path5.join)(domainPath, "context-instructions.md");
4474
- if ((0, import_fs5.existsSync)(ctxInstructions)) {
4255
+ const ctxInstructions = (0, import_path8.join)(domainPath, "context-instructions.md");
4256
+ if ((0, import_fs8.existsSync)(ctxInstructions)) {
4475
4257
  const desc = extractFirstHeading(ctxInstructions);
4476
4258
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/context-instructions.md`, desc, agent));
4477
4259
  }
4478
- const instructionsMd = (0, import_path5.join)(domainPath, "core", "instructions.md");
4479
- if ((0, import_fs5.existsSync)(instructionsMd)) {
4260
+ const instructionsMd = (0, import_path8.join)(domainPath, "core", "instructions.md");
4261
+ if ((0, import_fs8.existsSync)(instructionsMd)) {
4480
4262
  const desc = extractFirstHeading(instructionsMd);
4481
4263
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/instructions.md`, `${desc} (start here)`, agent));
4482
4264
  }
4483
- const coreDir = (0, import_path5.join)(domainPath, "core");
4484
- if ((0, import_fs5.existsSync)(coreDir)) {
4265
+ const coreDir = (0, import_path8.join)(domainPath, "core");
4266
+ if ((0, import_fs8.existsSync)(coreDir)) {
4485
4267
  const coreFiles = collectMdFiles(coreDir).filter((f) => f !== "instructions.md" && !f.startsWith("instructions/"));
4486
4268
  for (const file of coreFiles) {
4487
- const desc = extractFirstHeading((0, import_path5.join)(coreDir, file));
4269
+ const desc = extractFirstHeading((0, import_path8.join)(coreDir, file));
4488
4270
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/${file}`, desc, agent));
4489
4271
  }
4490
4272
  }
4491
- const refDir = (0, import_path5.join)(domainPath, "reference");
4492
- if ((0, import_fs5.existsSync)(refDir)) {
4273
+ const refDir = (0, import_path8.join)(domainPath, "reference");
4274
+ if ((0, import_fs8.existsSync)(refDir)) {
4493
4275
  const refFiles = collectMdFiles(refDir);
4494
4276
  if (refFiles.length > 0) {
4495
4277
  domainFiles.push(``);
4496
4278
  domainFiles.push(`**Reference & cheat sheets:**`);
4497
4279
  for (const file of refFiles) {
4498
- const desc = extractFirstHeading((0, import_path5.join)(refDir, file));
4280
+ const desc = extractFirstHeading((0, import_path8.join)(refDir, file));
4499
4281
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/reference/${file}`, desc, agent));
4500
4282
  }
4501
4283
  }
@@ -4517,73 +4299,25 @@ function buildMCPSection(mcpConfig) {
4517
4299
  if (entries.length === 0) return "";
4518
4300
  const lines2 = [`## MCP Servers`, ``, `The following MCP servers are available. Use them proactively when relevant:`, ``];
4519
4301
  for (const [name, config] of entries) {
4520
- const description = config.description;
4521
- const useWhen = config.useWhen;
4522
- if (!description) continue;
4302
+ if (!config.description) continue;
4523
4303
  lines2.push(`### ${name}`);
4524
- lines2.push(description);
4525
- if (useWhen) lines2.push(`**Use when**: ${useWhen}`);
4304
+ lines2.push(config.description);
4305
+ if (config.useWhen) lines2.push(`**Use when**: ${config.useWhen}`);
4526
4306
  lines2.push(``);
4527
4307
  }
4528
4308
  return lines2.join("\n");
4529
4309
  }
4530
- function buildFactorySection(agent) {
4531
- if (agent === "github-copilot") {
4532
- return [
4533
- `## Factory Dev Workflow`,
4534
- ``,
4535
- `Factory is installed. Skills are in \`factory/skills/\`, templates in \`factory/templates/\`.`,
4536
- ``,
4537
- `Read \`factory/README.md\` and \`factory/PROTOCOL.md\` for usage instructions.`,
4538
- ``,
4539
- `Available skills:`,
4540
- `- \`factory-advisor\` \u2014 start or resume the workflow (main entry point)`,
4541
- `- \`factory-init\` \u2014 initialize project state`,
4542
- `- \`factory-planner\` \u2014 create story plan`,
4543
- `- \`factory-executor\` \u2014 execute development tasks`,
4544
- `- \`factory-verifier\` \u2014 code review`,
4545
- `- \`factory-completer\` \u2014 finalize and complete work`,
4546
- ``
4547
- ].join("\n");
4548
- }
4549
- return [
4550
- `## Factory Dev Workflow`,
4551
- ``,
4552
- `Factory is installed. Use \`/factory-advisor\` to start or resume the workflow.`,
4553
- ``,
4554
- `Available commands:`,
4555
- `- \`/factory-advisor\` \u2014 start or resume the workflow (main entry point)`,
4556
- `- \`/factory-flow\` \u2014 guided workflow execution`,
4557
- `- \`/factory-feature-dev\` \u2014 add a feature to an existing project`,
4558
- `- \`/factory-dev\` \u2014 execute development tasks`,
4559
- `- \`/factory-progress\` \u2014 check status`,
4560
- `- \`/factory-init-state\` \u2014 initialize state for a brownfield project`,
4561
- `- \`/factory-analyst\` \u2014 validate requirements`,
4562
- `- \`/factory-architect\` \u2014 design architecture`,
4563
- `- \`/factory-planner\` \u2014 create story plan`,
4564
- `- \`/factory-verifier\` \u2014 code review`,
4565
- `- \`/factory-security\` \u2014 OWASP security review`,
4566
- `- \`/factory-test-analyst\` \u2014 test quality review`,
4567
- `- \`/factory-explorer\` \u2014 explore and understand code`,
4568
- `- \`/factory-docs\` \u2014 documentation hub`,
4569
- `- \`/factory-docs-generate\` \u2014 auto-generate documentation`,
4570
- `- \`/factory-docs-manual\` \u2014 manual documentation workflow`,
4571
- `- \`/factory-office\` \u2014 office mode discussion`,
4572
- ``
4573
- ].join("\n");
4574
- }
4575
- function buildCombinedInstructions(domains, mcpConfig, agent, projectPath, factoryInstalled) {
4576
- const contextsDir = (0, import_path5.join)(projectPath, "_ai-context");
4310
+ function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4311
+ const contextsDir = (0, import_path8.join)(projectPath, "_ai-context");
4577
4312
  const lines2 = [`# AI Development Instructions`, ``, `> Generated by One-Shot Installer`, ``];
4578
4313
  lines2.push(buildContextRefsSection(domains, agent, contextsDir));
4579
4314
  if (Object.keys(mcpConfig).length > 0) lines2.push(buildMCPSection(mcpConfig));
4580
- if (factoryInstalled) lines2.push(buildFactorySection(agent));
4581
4315
  return lines2.join("\n");
4582
4316
  }
4583
4317
 
4584
4318
  // src/steps/setup/write-mcp-config.ts
4585
- var import_fs6 = require("fs");
4586
- var import_path6 = require("path");
4319
+ var import_fs9 = require("fs");
4320
+ var import_path9 = require("path");
4587
4321
  var red2 = (text) => `\x1B[31m${text}\x1B[0m`;
4588
4322
  var write_mcp_config_default = defineStep({
4589
4323
  name: "write-mcp-config",
@@ -4592,15 +4326,15 @@ var write_mcp_config_default = defineStep({
4592
4326
  execute: async (ctx) => {
4593
4327
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4594
4328
  const target = getAgentTarget(ctx.config.agent);
4595
- const mcpJsonPath = (0, import_path6.join)(ctx.config.projectPath, target.mcpConfig);
4329
+ const mcpJsonPath = (0, import_path9.join)(ctx.config.projectPath, target.mcpConfig);
4596
4330
  if (target.mcpDir) {
4597
- const dir = (0, import_path6.join)(ctx.config.projectPath, target.mcpDir);
4598
- if (!(0, import_fs6.existsSync)(dir)) (0, import_fs6.mkdirSync)(dir, { recursive: true });
4331
+ const dir = (0, import_path9.join)(ctx.config.projectPath, target.mcpDir);
4332
+ if (!(0, import_fs9.existsSync)(dir)) (0, import_fs9.mkdirSync)(dir, { recursive: true });
4599
4333
  }
4600
4334
  let existingFile = {};
4601
- if ((0, import_fs6.existsSync)(mcpJsonPath)) {
4335
+ if ((0, import_fs9.existsSync)(mcpJsonPath)) {
4602
4336
  try {
4603
- existingFile = JSON.parse((0, import_fs6.readFileSync)(mcpJsonPath, "utf-8"));
4337
+ existingFile = JSON.parse((0, import_fs9.readFileSync)(mcpJsonPath, "utf-8"));
4604
4338
  } catch {
4605
4339
  const box = [
4606
4340
  "",
@@ -4626,7 +4360,7 @@ var write_mcp_config_default = defineStep({
4626
4360
  }
4627
4361
  const mergedServers = { ...existingServers, ...newServers };
4628
4362
  const outputFile = { ...existingFile, [target.mcpRootKey]: mergedServers };
4629
- (0, import_fs6.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
4363
+ (0, import_fs9.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
4630
4364
  ctx.installed.mcpServersAdded = addedServers;
4631
4365
  return {
4632
4366
  status: "success",
@@ -4636,7 +4370,7 @@ var write_mcp_config_default = defineStep({
4636
4370
  });
4637
4371
 
4638
4372
  // src/steps/setup/run-mcp-install-commands.ts
4639
- var import_child_process4 = require("child_process");
4373
+ var import_child_process3 = require("child_process");
4640
4374
  var run_mcp_install_commands_default = defineStep({
4641
4375
  name: "run-mcp-install-commands",
4642
4376
  label: "Registering MCP servers with Claude Code",
@@ -4658,7 +4392,7 @@ var run_mcp_install_commands_default = defineStep({
4658
4392
  for (const [name, cfg] of Object.entries(filtered)) {
4659
4393
  const cmd = cfg.installCommand;
4660
4394
  if (typeof cmd !== "string" || cmd.length === 0) continue;
4661
- const result = (0, import_child_process4.spawnSync)(cmd, {
4395
+ const result = (0, import_child_process3.spawnSync)(cmd, {
4662
4396
  shell: true,
4663
4397
  stdio: "pipe",
4664
4398
  encoding: "utf-8",
@@ -4672,7 +4406,6 @@ var run_mcp_install_commands_default = defineStep({
4672
4406
  failed.push({ name, reason });
4673
4407
  }
4674
4408
  }
4675
- ctx.installed.mcpInstallCommandsRun = { succeeded, failed };
4676
4409
  if (failed.length === 0) {
4677
4410
  return {
4678
4411
  status: "success",
@@ -4691,13 +4424,12 @@ var run_mcp_install_commands_default = defineStep({
4691
4424
  }
4692
4425
  });
4693
4426
  function isClaudeCliAvailable() {
4694
- const check2 = (0, import_child_process4.spawnSync)("claude --version", { shell: true, stdio: "ignore" });
4427
+ const check2 = (0, import_child_process3.spawnSync)("claude --version", { shell: true, stdio: "ignore" });
4695
4428
  return check2.status === 0;
4696
4429
  }
4697
4430
 
4698
4431
  // src/steps/setup/update-gitignore.ts
4699
- var import_fs7 = require("fs");
4700
- var import_path7 = require("path");
4432
+ var import_path10 = require("path");
4701
4433
  var MARKER_START2 = "# one-shot-installer:start";
4702
4434
  var MARKER_END2 = "# one-shot-installer:end";
4703
4435
  var CORE_GITIGNORE_ENTRIES = [
@@ -4714,58 +4446,52 @@ var CORE_GITIGNORE_ENTRIES = [
4714
4446
  ".claude/plans/",
4715
4447
  ".claude/settings.local.json"
4716
4448
  ];
4717
- var LEGACY_FACTORY_GITIGNORE_ENTRIES = [
4718
- "# Factory runtime state (generated by workflow, not committed)",
4719
- "factory/STATE.md",
4720
- "factory/PROJECT.md",
4721
- "factory/REQUIREMENTS.md",
4722
- "factory/ARCHITECTURE.md",
4723
- "factory/ROADMAP.md",
4724
- "factory/PLAN.md",
4725
- "factory/GATE-REPORT.md",
4726
- "factory/VERIFY-REPORT.md",
4727
- "factory/COMPLETION.md",
4728
- "factory/summaries/",
4729
- "factory/ai-context/"
4730
- ];
4731
4449
  var update_gitignore_default = defineStep({
4732
4450
  name: "update-gitignore",
4733
4451
  label: "Updating .gitignore",
4734
4452
  execute: async (ctx) => {
4735
- const gitignorePath = (0, import_path7.join)(ctx.config.projectPath, ".gitignore");
4736
- const factoryInstalled = ctx.installed.factoryInstalled ?? false;
4737
- const factoryBundleManaged = ctx.installed.factoryBundleManaged ?? false;
4738
- const includeLegacyFactory = factoryInstalled && !factoryBundleManaged;
4739
- const entries = includeLegacyFactory ? [...CORE_GITIGNORE_ENTRIES, "", ...LEGACY_FACTORY_GITIGNORE_ENTRIES] : CORE_GITIGNORE_ENTRIES;
4740
- const block = [MARKER_START2, ...entries, MARKER_END2].join("\n");
4741
- if (!(0, import_fs7.existsSync)(gitignorePath)) {
4742
- (0, import_fs7.writeFileSync)(gitignorePath, block + "\n", "utf-8");
4743
- return { status: "success" };
4744
- }
4745
- const existing = (0, import_fs7.readFileSync)(gitignorePath, "utf-8");
4746
- const start = existing.indexOf(MARKER_START2);
4747
- const end = existing.indexOf(MARKER_END2);
4748
- if (start !== -1 && end !== -1 && end > start) {
4749
- const updated = existing.slice(0, start) + block + existing.slice(end + MARKER_END2.length);
4750
- (0, import_fs7.writeFileSync)(gitignorePath, updated, "utf-8");
4751
- } else {
4752
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
4753
- (0, import_fs7.writeFileSync)(gitignorePath, existing + separator + block + "\n", "utf-8");
4754
- }
4453
+ const gitignorePath = (0, import_path10.join)(ctx.config.projectPath, ".gitignore");
4454
+ upsertMarkerBlock(gitignorePath, MARKER_START2, MARKER_END2, CORE_GITIGNORE_ENTRIES.join("\n"));
4755
4455
  return { status: "success" };
4756
4456
  }
4757
4457
  });
4758
4458
 
4759
4459
  // src/steps/setup/write-state.ts
4760
- var import_fs8 = require("fs");
4761
- var import_path8 = require("path");
4460
+ var import_fs10 = require("fs");
4461
+ var import_path11 = require("path");
4762
4462
  var import_os2 = require("os");
4763
- var INSTALLER_VERSION = "0.6.0";
4463
+
4464
+ // package.json
4465
+ var package_default = {
4466
+ name: "one-shot-setup",
4467
+ version: "0.6.0",
4468
+ description: "One-shot installation tool for The Machine toolchain",
4469
+ type: "module",
4470
+ main: "src/index.ts",
4471
+ scripts: {
4472
+ dev: "tsx src/index.ts",
4473
+ build: 'esbuild src/index.ts --bundle --platform=node --banner:js="#!/usr/bin/env node" --outfile=wrapper/bin/installer.js',
4474
+ "publish:wrapper": "cd wrapper && npm publish"
4475
+ },
4476
+ dependencies: {
4477
+ "@inquirer/prompts": "^5.0.0"
4478
+ },
4479
+ devDependencies: {
4480
+ "@types/node": "^20.11.0",
4481
+ esbuild: "latest",
4482
+ tsx: "latest"
4483
+ },
4484
+ engines: {
4485
+ node: ">=18.0.0"
4486
+ }
4487
+ };
4488
+
4489
+ // src/steps/setup/write-state.ts
4764
4490
  var write_state_default = defineStep({
4765
4491
  name: "write-state",
4766
4492
  label: "Saving installation state",
4767
4493
  execute: async (ctx) => {
4768
- const statePath = (0, import_path8.join)(ctx.config.projectPath, ".one-shot-state.json");
4494
+ const statePath = (0, import_path11.join)(ctx.config.projectPath, ".one-shot-state.json");
4769
4495
  const mcpServersAdded = ctx.installed.mcpServersAdded ?? [];
4770
4496
  const filteredMcpConfig = Object.fromEntries(
4771
4497
  Object.entries(ctx.config.mcpConfig).filter(([name]) => mcpServersAdded.includes(name))
@@ -4773,7 +4499,7 @@ var write_state_default = defineStep({
4773
4499
  const target = getAgentTarget(ctx.config.agent);
4774
4500
  const state = {
4775
4501
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4776
- installerVersion: INSTALLER_VERSION,
4502
+ installerVersion: package_default.version,
4777
4503
  releaseVersion: ctx.config.releaseVersion,
4778
4504
  contextReleaseVersion: ctx.installed.contextReleaseVersion ?? null,
4779
4505
  projectPath: ctx.config.projectPath,
@@ -4788,10 +4514,10 @@ var write_state_default = defineStep({
4788
4514
  mcpConfig: target.mcpConfig,
4789
4515
  contexts: "_ai-context/",
4790
4516
  factory: ctx.installed.factoryInstalled ? "factory/" : null,
4791
- globalConfig: (0, import_path8.join)((0, import_os2.homedir)(), ".one-shot-installer")
4517
+ globalConfig: (0, import_path11.join)((0, import_os2.homedir)(), ".one-shot-installer")
4792
4518
  }
4793
4519
  };
4794
- (0, import_fs8.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4520
+ (0, import_fs10.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4795
4521
  return { status: "success" };
4796
4522
  }
4797
4523
  });
@@ -4806,27 +4532,27 @@ var dim = (text) => `\x1B[2m${text}\x1B[0m`;
4806
4532
  var yellow = (text) => `\x1B[33m${text}\x1B[0m`;
4807
4533
  var green = (text) => `\x1B[32m${text}\x1B[0m`;
4808
4534
  function detectMarkerFileMode(filePath, markerStart) {
4809
- if (!(0, import_fs9.existsSync)(filePath)) return green("create");
4810
- const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
4535
+ if (!(0, import_fs11.existsSync)(filePath)) return green("create");
4536
+ const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
4811
4537
  if (content.includes(markerStart)) return yellow("update");
4812
4538
  return yellow("append");
4813
4539
  }
4814
4540
  function detectMCPFileMode(filePath) {
4815
- if (!(0, import_fs9.existsSync)(filePath)) return green("create");
4541
+ if (!(0, import_fs11.existsSync)(filePath)) return green("create");
4816
4542
  return yellow("merge");
4817
4543
  }
4818
4544
  function detectContextMode(projectPath, domain) {
4819
- const contextDir = (0, import_path9.join)(projectPath, "_ai-context", domain.toUpperCase());
4820
- const contextDirLower = (0, import_path9.join)(projectPath, "_ai-context", domain);
4821
- if ((0, import_fs9.existsSync)(contextDir) || (0, import_fs9.existsSync)(contextDirLower)) return yellow("overwrite");
4545
+ const contextDir = (0, import_path12.join)(projectPath, "_ai-context", domain.toUpperCase());
4546
+ const contextDirLower = (0, import_path12.join)(projectPath, "_ai-context", domain);
4547
+ if ((0, import_fs11.existsSync)(contextDir) || (0, import_fs11.existsSync)(contextDirLower)) return yellow("overwrite");
4822
4548
  return green("create");
4823
4549
  }
4824
4550
  function waitForEnter() {
4825
4551
  const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
4826
- return new Promise((resolve5) => {
4552
+ return new Promise((resolve4) => {
4827
4553
  rl.question(" Press Enter to close...", () => {
4828
4554
  rl.close();
4829
- resolve5();
4555
+ resolve4();
4830
4556
  });
4831
4557
  });
4832
4558
  }
@@ -4916,13 +4642,13 @@ async function collectInputs(options, releaseVersion, factoryAvailable = false)
4916
4642
  }
4917
4643
  const projectInput = await esm_default4({
4918
4644
  message: "Project directory:",
4919
- default: (0, import_path9.resolve)(process.cwd())
4645
+ default: (0, import_path12.resolve)(process.cwd())
4920
4646
  });
4921
4647
  return {
4922
4648
  technologies: selectedTechnologies,
4923
4649
  agent,
4924
4650
  azureDevOpsOrg,
4925
- projectPath: (0, import_path9.resolve)(projectInput),
4651
+ projectPath: (0, import_path12.resolve)(projectInput),
4926
4652
  baseMcpServers: options.baseMcpServers,
4927
4653
  mcpConfig,
4928
4654
  releaseVersion,
@@ -4936,9 +4662,9 @@ async function previewAndConfirm(config, options) {
4936
4662
  const instructionFile = target.instructions;
4937
4663
  const mcpConfigFile = target.mcpConfig;
4938
4664
  const serverEntries = Object.entries(config.mcpConfig);
4939
- const instructionFilePath = (0, import_path9.join)(config.projectPath, instructionFile);
4940
- const mcpConfigFilePath = (0, import_path9.join)(config.projectPath, mcpConfigFile);
4941
- const gitignorePath = (0, import_path9.join)(config.projectPath, ".gitignore");
4665
+ const instructionFilePath = (0, import_path12.join)(config.projectPath, instructionFile);
4666
+ const mcpConfigFilePath = (0, import_path12.join)(config.projectPath, mcpConfigFile);
4667
+ const gitignorePath = (0, import_path12.join)(config.projectPath, ".gitignore");
4942
4668
  const instructionMode = detectMarkerFileMode(instructionFilePath, "<!-- one-shot-installer:start -->");
4943
4669
  const mcpMode = detectMCPFileMode(mcpConfigFilePath);
4944
4670
  const gitignoreMode = detectMarkerFileMode(gitignorePath, "# one-shot-installer:start");
@@ -4964,7 +4690,7 @@ async function previewAndConfirm(config, options) {
4964
4690
  console.log(` ${mcpConfigFile.padEnd(domainColWidth + 14)}${mcpMode}`);
4965
4691
  console.log(` ${".gitignore".padEnd(domainColWidth + 14)}${gitignoreMode}`);
4966
4692
  if (config.installFactory) {
4967
- const factoryExists = (0, import_fs9.existsSync)((0, import_path9.join)(config.projectPath, "factory"));
4693
+ const factoryExists = (0, import_fs11.existsSync)((0, import_path12.join)(config.projectPath, "factory"));
4968
4694
  const factoryMode = factoryExists ? yellow("overwrite") : green("create");
4969
4695
  console.log(` ${"factory/".padEnd(domainColWidth + 14)}${factoryMode}`);
4970
4696
  }
@@ -4983,6 +4709,7 @@ async function previewAndConfirm(config, options) {
4983
4709
  function printSummary(result, config) {
4984
4710
  const failed = result.entries.filter((e) => e.result.status === "failed");
4985
4711
  const succeeded = result.entries.filter((e) => e.result.status === "success");
4712
+ const skipped = result.entries.filter((e) => e.result.status === "skipped");
4986
4713
  const hasErrors = failed.length > 0;
4987
4714
  console.log("\u2500".repeat(48));
4988
4715
  console.log(hasErrors ? " Done (with errors).\n" : " Done.\n");
@@ -4990,6 +4717,10 @@ function printSummary(result, config) {
4990
4717
  const msg = entry.result.message ? ` ${entry.result.message}` : "";
4991
4718
  console.log(` ${entry.label}${msg}`);
4992
4719
  }
4720
+ for (const entry of skipped) {
4721
+ const detail = entry.result.detail ? ` ${entry.result.detail}` : "";
4722
+ console.log(` ${entry.label} (skipped)${detail}`);
4723
+ }
4993
4724
  if (hasErrors) {
4994
4725
  console.log("\n What went wrong:");
4995
4726
  for (const entry of failed) {