dlw-machine-setup 0.4.11 → 0.5.7

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 (3) hide show
  1. package/README.md +27 -84
  2. package/bin/installer.js +410 -104
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,108 +1,51 @@
1
- # @DLW-INT-SAP-DEV/one-shot-installer
1
+ # dlw-machine-setup
2
2
 
3
- NPM wrapper for the One-Shot Setup installer. This package downloads and executes the appropriate binary from GitHub Releases based on your platform.
3
+ NPM wrapper for the One-Shot Setup installer. This package runs a CLI wizard that configures AI-enhanced development with SAP-specific context files and MCP servers.
4
4
 
5
- ## Installation
6
-
7
- ### Using npx (Recommended)
8
-
9
- No installation required - run directly:
5
+ ## Usage
10
6
 
11
7
  ```bash
12
- npx @DLW-INT-SAP-DEV/one-shot-installer
8
+ npx dlw-machine-setup@latest
13
9
  ```
14
10
 
15
- ### Global Installation
16
-
17
- ```bash
18
- npm install -g @DLW-INT-SAP-DEV/one-shot-installer
19
- one-shot-installer
20
- ```
11
+ Requires Node.js >=18. No additional runtime or binaries needed.
21
12
 
22
- ### Local Installation
13
+ ## What It Does
23
14
 
24
- ```bash
25
- npm install @DLW-INT-SAP-DEV/one-shot-installer
26
- npx one-shot-installer
27
- ```
15
+ The wizard guides you through:
28
16
 
29
- ## Authentication
17
+ 1. **GitHub authentication** — one-time OAuth Device Flow (token cached for future runs)
18
+ 2. **Persona selection** — choose your SAP development domains (SAPUI5, CAP, ABAP, BTP, SAP DM, Forms)
19
+ 3. **AI tool selection** — Claude Code, GitHub Copilot, or Cursor
20
+ 4. **Azure DevOps org** — optional, for Azure DevOps MCP integration
21
+ 5. **Project directory** — where to install (defaults to current directory)
30
22
 
31
- This package downloads binaries from a private GitHub repository. Authentication is handled automatically:
23
+ After confirming a preview, it downloads context archives from GitHub Releases and writes the appropriate config files for your AI tool.
32
24
 
33
- 1. **First run**: The binary will prompt you to authenticate via GitHub OAuth Device Flow
34
- 2. **Subsequent runs**: Your token is cached at `~/.one-shot-installer/github-token.json`
25
+ ## Authentication
35
26
 
36
- ### Alternative: Environment Variable
27
+ On first run, the installer prompts you to authenticate via GitHub OAuth Device Flow (browser-based). Your token is cached at `~/.one-shot-installer/github-token.json` for all future runs.
37
28
 
38
- You can also provide a GitHub token via environment variable:
29
+ Alternatively, set the `GITHUB_TOKEN` environment variable:
39
30
 
40
31
  ```bash
32
+ # macOS/Linux
33
+ GITHUB_TOKEN="your_token_here" npx dlw-machine-setup@latest
34
+
41
35
  # Windows (PowerShell)
42
36
  $env:GITHUB_TOKEN="your_token_here"
43
- npx @DLW-INT-SAP-DEV/one-shot-installer
44
-
45
- # macOS/Linux
46
- GITHUB_TOKEN="your_token_here" npx @DLW-INT-SAP-DEV/one-shot-installer
37
+ npx dlw-machine-setup@latest
47
38
  ```
48
39
 
49
- ## How It Works
50
-
51
- 1. Detects your platform (Windows, macOS, Linux) and architecture
52
- 2. Downloads the appropriate binary from GitHub Releases (or uses cached version)
53
- 3. Executes the installer
54
- 4. The installer guides you through:
55
- - Selecting technology domains (SAPUI5, CAP, ABAP, etc.)
56
- - Choosing your AI assistant
57
- - Installing MCP server configuration
58
-
59
- ## Supported Platforms
60
-
61
- - **Windows**: x64
62
- - **macOS**: x64 (Intel), arm64 (Apple Silicon)
63
- - **Linux**: x64
64
-
65
- ## Cache Location
66
-
67
- Binaries are cached at:
68
- - **Windows**: `%USERPROFILE%\.one-shot-installer\bin\`
69
- - **macOS/Linux**: `~/.one-shot-installer/bin/`
70
-
71
40
  ## Troubleshooting
72
41
 
73
- ### "Failed to download binary"
74
-
75
- If you see authentication errors:
76
-
77
- 1. Make sure you have access to the repository
78
- 2. Run the installer once to authenticate via OAuth
79
- 3. Or set `GITHUB_TOKEN` environment variable with a valid GitHub token
80
-
81
- ### "Unsupported platform"
82
-
83
- The installer currently supports:
84
- - Windows 10/11 (x64)
85
- - macOS 10.15+ (Intel and Apple Silicon)
86
- - Linux x64 (Ubuntu, Debian, RHEL, etc.)
87
-
88
- ### Binary not executing
89
-
90
- On macOS/Linux, if you get permission errors:
91
- ```bash
92
- chmod +x ~/.one-shot-installer/bin/machine-setup-*
93
- ```
94
-
95
- ## What Gets Installed
96
-
97
- The installer provides:
98
- - **Technology-specific context files** (documentation, patterns, best practices)
99
- - **MCP server configuration** for AI assistants (Claude, Copilot, Cursor)
100
- - **Setup instructions** for your development environment
42
+ | Problem | Solution |
43
+ |---|---|
44
+ | Authentication fails | Delete `~/.one-shot-installer/github-token.json` and re-run |
45
+ | Context downloads fail | Check network connectivity and GitHub access |
46
+ | SSO error | Ask your org admin to approve the OAuth app in GitHub org settings |
47
+ | Stale cached version | Always use `npx dlw-machine-setup@latest` to bypass npx cache |
101
48
 
102
49
  ## License
103
50
 
104
- Internal use only - DLW-INT-SAP-DEV organization
105
-
106
- ## Support
107
-
108
- For issues or questions, open an issue on GitHub.
51
+ Internal use only DLW-INT-SAP-DEV organization
package/bin/installer.js CHANGED
@@ -2749,13 +2749,13 @@ var PromisePolyfill = class extends Promise {
2749
2749
  // Available starting from Node 22
2750
2750
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
2751
2751
  static withResolver() {
2752
- let resolve3;
2752
+ let resolve4;
2753
2753
  let reject;
2754
2754
  const promise = new Promise((res, rej) => {
2755
- resolve3 = res;
2755
+ resolve4 = res;
2756
2756
  reject = rej;
2757
2757
  });
2758
- return { promise, resolve: resolve3, reject };
2758
+ return { promise, resolve: resolve4, reject };
2759
2759
  }
2760
2760
  };
2761
2761
 
@@ -2772,7 +2772,7 @@ function createPrompt(view) {
2772
2772
  output
2773
2773
  });
2774
2774
  const screen = new ScreenManager(rl);
2775
- const { promise, resolve: resolve3, reject } = PromisePolyfill.withResolver();
2775
+ const { promise, resolve: resolve4, reject } = PromisePolyfill.withResolver();
2776
2776
  const cancel = () => reject(new CancelPromptError());
2777
2777
  if (signal) {
2778
2778
  const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
@@ -2796,7 +2796,7 @@ function createPrompt(view) {
2796
2796
  cycle(() => {
2797
2797
  try {
2798
2798
  const nextView = view(config, (value) => {
2799
- setImmediate(() => resolve3(value));
2799
+ setImmediate(() => resolve4(value));
2800
2800
  });
2801
2801
  const [content, bottomContent] = typeof nextView === "string" ? [nextView] : nextView;
2802
2802
  screen.render(content, bottomContent);
@@ -3240,9 +3240,10 @@ ${page}${helpTipBottom}${choiceDescription}${import_ansi_escapes3.default.cursor
3240
3240
  });
3241
3241
 
3242
3242
  // src/index.ts
3243
- var import_fs6 = require("fs");
3243
+ var import_fs7 = require("fs");
3244
3244
  var import_readline = require("readline");
3245
- var import_path6 = require("path");
3245
+ var import_path7 = require("path");
3246
+ var import_os2 = require("os");
3246
3247
 
3247
3248
  // src/utils/fetch.ts
3248
3249
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -3279,7 +3280,7 @@ async function fetchWithRetry(url, options = {}, maxRetries = DEFAULT_MAX_RETRIE
3279
3280
  }
3280
3281
  if (attempt < maxRetries) {
3281
3282
  const delay = retryDelayMs * Math.pow(2, attempt - 1);
3282
- await new Promise((resolve3) => setTimeout(resolve3, delay));
3283
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
3283
3284
  }
3284
3285
  }
3285
3286
  throw lastError || new Error("Failed after maximum retries");
@@ -3316,31 +3317,31 @@ async function fetchWizardOptions(token, repo) {
3316
3317
 
3317
3318
  // src/utils/data/discover-repo.ts
3318
3319
  var GITHUB_ORG = "DLW-INT-SAP-DEV";
3319
- var REPO_TOPIC = "one-shot-data";
3320
- async function discoverRepo(token) {
3320
+ var DEFAULT_TOPIC = "one-shot-data";
3321
+ async function discoverRepo(token, topic = DEFAULT_TOPIC) {
3321
3322
  const headers = {
3322
3323
  "Accept": "application/vnd.github+json",
3323
3324
  "Authorization": `Bearer ${token}`
3324
3325
  };
3325
- const searchResult = await trySearchAPI(headers);
3326
+ const searchResult = await trySearchAPI(headers, topic);
3326
3327
  if (searchResult) return searchResult;
3327
- const listResult = await tryOrgReposList(headers);
3328
+ const listResult = await tryOrgReposList(headers, topic);
3328
3329
  if (listResult) return listResult;
3329
3330
  throw new Error(
3330
- "No data repository found. Verify your GitHub account has access to the organization and the repository topic is configured."
3331
+ `No repository with topic "${topic}" found. Verify your GitHub account has access to the organization and the repository topic is configured.`
3331
3332
  );
3332
3333
  }
3333
- async function trySearchAPI(headers) {
3334
+ async function trySearchAPI(headers, topic) {
3334
3335
  try {
3335
3336
  const response = await fetchWithRetry(
3336
- `https://api.github.com/search/repositories?q=topic:${REPO_TOPIC}+org:${GITHUB_ORG}`,
3337
+ `https://api.github.com/search/repositories?q=topic:${topic}+org:${GITHUB_ORG}`,
3337
3338
  { headers }
3338
3339
  );
3339
3340
  if (!response.ok) return null;
3340
3341
  const data = await response.json();
3341
3342
  if (data.total_count === 1) return data.items[0].full_name;
3342
3343
  if (data.total_count > 1) {
3343
- throw new Error("Multiple data repositories found. Contact your administrator.");
3344
+ throw new Error(`Multiple repositories with topic "${topic}" found. Contact your administrator.`);
3344
3345
  }
3345
3346
  return null;
3346
3347
  } catch (error) {
@@ -3348,7 +3349,7 @@ async function trySearchAPI(headers) {
3348
3349
  return null;
3349
3350
  }
3350
3351
  }
3351
- async function tryOrgReposList(headers) {
3352
+ async function tryOrgReposList(headers, topic) {
3352
3353
  try {
3353
3354
  const matched = [];
3354
3355
  let page = 1;
@@ -3361,7 +3362,7 @@ async function tryOrgReposList(headers) {
3361
3362
  const repos = await response.json();
3362
3363
  if (repos.length === 0) break;
3363
3364
  for (const repo of repos) {
3364
- if (repo.topics?.includes(REPO_TOPIC)) {
3365
+ if (repo.topics?.includes(topic)) {
3365
3366
  matched.push(repo.full_name);
3366
3367
  }
3367
3368
  }
@@ -3370,7 +3371,7 @@ async function tryOrgReposList(headers) {
3370
3371
  }
3371
3372
  if (matched.length === 1) return matched[0];
3372
3373
  if (matched.length > 1) {
3373
- throw new Error("Multiple data repositories found. Contact your administrator.");
3374
+ throw new Error(`Multiple repositories with topic "${topic}" found. Contact your administrator.`);
3374
3375
  }
3375
3376
  return null;
3376
3377
  } catch (error) {
@@ -3450,7 +3451,7 @@ async function pollForToken(deviceCode, interval) {
3450
3451
  const maxAttempts = 60;
3451
3452
  let attempts = 0;
3452
3453
  while (attempts < maxAttempts) {
3453
- await new Promise((resolve3) => setTimeout(resolve3, interval * 1e3));
3454
+ await new Promise((resolve4) => setTimeout(resolve4, interval * 1e3));
3454
3455
  const response = await fetchWithTimeout(GITHUB_TOKEN_URL, {
3455
3456
  method: "POST",
3456
3457
  headers: {
@@ -3715,9 +3716,210 @@ function copyDirectory(source, target) {
3715
3716
  return { added, overwritten };
3716
3717
  }
3717
3718
 
3718
- // src/utils/setup/setup-mcp.ts
3719
+ // src/utils/data/fetch-factory.ts
3719
3720
  var import_fs3 = require("fs");
3720
3721
  var import_path3 = require("path");
3722
+ var import_child_process2 = require("child_process");
3723
+ var MIN_FILE_SIZE2 = 1024;
3724
+ var FACTORY_ASSET_NAME = "factory.tar.gz";
3725
+ async function fetchFactory(options) {
3726
+ const { token, repo, targetDir } = options;
3727
+ const result = {
3728
+ success: false,
3729
+ filesInstalled: []
3730
+ };
3731
+ const tarCheck = (0, import_child_process2.spawnSync)("tar", ["--version"], { stdio: "ignore" });
3732
+ if (tarCheck.status !== 0) {
3733
+ result.failureReason = "tar command not found";
3734
+ return result;
3735
+ }
3736
+ const headers = {
3737
+ "Accept": "application/vnd.github+json",
3738
+ "Authorization": `Bearer ${token}`
3739
+ };
3740
+ const releaseUrl = `https://api.github.com/repos/${repo}/releases/latest`;
3741
+ const releaseResponse = await fetchWithRetry(releaseUrl, { headers });
3742
+ if (!releaseResponse.ok) {
3743
+ result.failureReason = `GitHub API error (${releaseResponse.status})`;
3744
+ return result;
3745
+ }
3746
+ const releaseData = await releaseResponse.json();
3747
+ const asset = releaseData.assets?.find((a) => a.name === FACTORY_ASSET_NAME);
3748
+ if (!asset) {
3749
+ result.failureReason = `${FACTORY_ASSET_NAME} not found in release`;
3750
+ return result;
3751
+ }
3752
+ const tempDir = (0, import_path3.join)(targetDir, ".temp-factory-download");
3753
+ try {
3754
+ if ((0, import_fs3.existsSync)(tempDir)) {
3755
+ (0, import_fs3.rmSync)(tempDir, { recursive: true, force: true });
3756
+ }
3757
+ (0, import_fs3.mkdirSync)(tempDir, { recursive: true });
3758
+ const downloadHeaders = {
3759
+ "Accept": "application/octet-stream",
3760
+ "Authorization": `Bearer ${token}`
3761
+ };
3762
+ const response = await fetchWithRetry(asset.url, { headers: downloadHeaders });
3763
+ if (!response.ok) {
3764
+ result.failureReason = `Download failed (${response.status})`;
3765
+ return result;
3766
+ }
3767
+ const archivePath = (0, import_path3.join)(tempDir, FACTORY_ASSET_NAME);
3768
+ const arrayBuffer = await response.arrayBuffer();
3769
+ (0, import_fs3.writeFileSync)(archivePath, Buffer.from(arrayBuffer));
3770
+ const stats = (0, import_fs3.statSync)(archivePath);
3771
+ if (stats.size < MIN_FILE_SIZE2) {
3772
+ result.failureReason = "Downloaded file corrupted (too small)";
3773
+ return result;
3774
+ }
3775
+ const listResult = (0, import_child_process2.spawnSync)("tar", ["-tzf", archivePath], { encoding: "utf8" });
3776
+ if (listResult.status !== 0) {
3777
+ result.failureReason = "Failed to read archive contents";
3778
+ return result;
3779
+ }
3780
+ const resolvedTempDir = (0, import_path3.resolve)(tempDir);
3781
+ for (const entry of listResult.stdout.split("\n").filter(Boolean)) {
3782
+ const entryPath = (0, import_path3.resolve)((0, import_path3.join)(tempDir, entry.replace(/\/$/, "")));
3783
+ if (!entryPath.startsWith(resolvedTempDir + import_path3.sep)) {
3784
+ result.failureReason = `Archive contains unsafe path: ${entry}`;
3785
+ return result;
3786
+ }
3787
+ }
3788
+ const extractResult = (0, import_child_process2.spawnSync)("tar", ["-xzf", archivePath, "-C", tempDir], { stdio: "ignore" });
3789
+ if (extractResult.status !== 0) {
3790
+ result.failureReason = "Archive extraction failed";
3791
+ return result;
3792
+ }
3793
+ const extractedEntries = (0, import_fs3.readdirSync)(tempDir);
3794
+ const extractedFolder = extractedEntries.find((e) => e.toLowerCase() === "factory");
3795
+ if (!extractedFolder) {
3796
+ result.failureReason = "No factory/ folder in archive";
3797
+ return result;
3798
+ }
3799
+ const extractedPath = (0, import_path3.join)(tempDir, extractedFolder);
3800
+ const claudeDir = (0, import_path3.join)(targetDir, ".claude");
3801
+ const factoryDir = (0, import_path3.join)(targetDir, "factory");
3802
+ const agentsSrc = (0, import_path3.join)(extractedPath, "agents");
3803
+ if ((0, import_fs3.existsSync)(agentsSrc)) {
3804
+ copyDirectory2(agentsSrc, (0, import_path3.join)(factoryDir, "agents"));
3805
+ copyAgentStubs(agentsSrc, (0, import_path3.join)(claudeDir, "agents"));
3806
+ result.filesInstalled.push(".claude/agents/", "factory/agents/");
3807
+ }
3808
+ const hooksSrc = (0, import_path3.join)(extractedPath, "hooks");
3809
+ if ((0, import_fs3.existsSync)(hooksSrc)) {
3810
+ copyDirectory2(hooksSrc, (0, import_path3.join)(claudeDir, "hooks"));
3811
+ result.filesInstalled.push(".claude/hooks/");
3812
+ }
3813
+ const skillsSrc = (0, import_path3.join)(extractedPath, "skills");
3814
+ if ((0, import_fs3.existsSync)(skillsSrc)) {
3815
+ copyDirectory2(skillsSrc, (0, import_path3.join)(claudeDir, "skills"));
3816
+ result.filesInstalled.push(".claude/skills/");
3817
+ }
3818
+ const workflowSrc = (0, import_path3.join)(extractedPath, "workflow");
3819
+ if ((0, import_fs3.existsSync)(workflowSrc)) {
3820
+ copyDirectory2(workflowSrc, (0, import_path3.join)(factoryDir, "workflow"));
3821
+ result.filesInstalled.push("factory/workflow/");
3822
+ }
3823
+ configureSettings(claudeDir);
3824
+ result.filesInstalled.push(".claude/settings.json");
3825
+ result.success = true;
3826
+ } catch (error) {
3827
+ result.failureReason = error instanceof Error ? error.message : String(error);
3828
+ } finally {
3829
+ if ((0, import_fs3.existsSync)(tempDir)) {
3830
+ try {
3831
+ (0, import_fs3.rmSync)(tempDir, { recursive: true, force: true });
3832
+ } catch {
3833
+ }
3834
+ }
3835
+ }
3836
+ return result;
3837
+ }
3838
+ function configureSettings(claudeDir) {
3839
+ const settingsPath = (0, import_path3.join)(claudeDir, "settings.json");
3840
+ let settings = {};
3841
+ if ((0, import_fs3.existsSync)(settingsPath)) {
3842
+ try {
3843
+ settings = JSON.parse((0, import_fs3.readFileSync)(settingsPath, "utf8"));
3844
+ } catch {
3845
+ }
3846
+ }
3847
+ if (!settings.hooks) settings.hooks = {};
3848
+ const stateAdvanceHook = {
3849
+ matcher: "Write|Edit",
3850
+ hooks: [{ type: "command", command: "node .claude/hooks/factory-state-advance.cjs" }]
3851
+ };
3852
+ const contextMonitorHook = {
3853
+ hooks: [{ type: "command", command: "node .claude/hooks/factory-context-monitor.cjs" }]
3854
+ };
3855
+ const stateGuardHook = {
3856
+ hooks: [{ type: "command", command: "node .claude/hooks/factory-state-guard.cjs" }]
3857
+ };
3858
+ for (const event of ["PostToolUse", "UserPromptSubmit"]) {
3859
+ if (settings.hooks[event]) {
3860
+ settings.hooks[event] = settings.hooks[event].filter(
3861
+ (h) => !h.hooks?.some(
3862
+ (hh) => hh.command?.includes("factory-state-advance") || hh.command?.includes("factory-context-monitor") || hh.command?.includes("factory-state-guard")
3863
+ )
3864
+ );
3865
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
3866
+ }
3867
+ }
3868
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
3869
+ settings.hooks.PostToolUse.unshift(stateAdvanceHook);
3870
+ if (!settings.hooks.UserPromptSubmit) {
3871
+ settings.hooks.UserPromptSubmit = [stateGuardHook, contextMonitorHook];
3872
+ } else {
3873
+ const hasGuard = settings.hooks.UserPromptSubmit.some(
3874
+ (h) => h.hooks?.some((hh) => hh.command?.includes("factory-state-guard"))
3875
+ );
3876
+ if (!hasGuard) settings.hooks.UserPromptSubmit.unshift(stateGuardHook);
3877
+ const hasMonitor = settings.hooks.UserPromptSubmit.some(
3878
+ (h) => h.hooks?.some((hh) => hh.command?.includes("factory-context-monitor"))
3879
+ );
3880
+ if (!hasMonitor) settings.hooks.UserPromptSubmit.push(contextMonitorHook);
3881
+ }
3882
+ settings.statusLine = { type: "command", command: "node .claude/hooks/factory-statusline.cjs" };
3883
+ if (!(0, import_fs3.existsSync)(claudeDir)) {
3884
+ (0, import_fs3.mkdirSync)(claudeDir, { recursive: true });
3885
+ }
3886
+ (0, import_fs3.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3887
+ }
3888
+ function copyAgentStubs(source, target) {
3889
+ if (!(0, import_fs3.existsSync)(target)) {
3890
+ (0, import_fs3.mkdirSync)(target, { recursive: true });
3891
+ }
3892
+ const entries = (0, import_fs3.readdirSync)(source, { withFileTypes: true });
3893
+ for (const entry of entries) {
3894
+ if (entry.isDirectory()) continue;
3895
+ if (!entry.name.endsWith(".md")) continue;
3896
+ const content = (0, import_fs3.readFileSync)((0, import_path3.join)(source, entry.name), "utf8");
3897
+ const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?\r?\n)---/);
3898
+ if (frontmatterMatch) {
3899
+ (0, import_fs3.writeFileSync)((0, import_path3.join)(target, entry.name), frontmatterMatch[0] + "\n");
3900
+ } else {
3901
+ }
3902
+ }
3903
+ }
3904
+ function copyDirectory2(source, target) {
3905
+ if (!(0, import_fs3.existsSync)(target)) {
3906
+ (0, import_fs3.mkdirSync)(target, { recursive: true });
3907
+ }
3908
+ const entries = (0, import_fs3.readdirSync)(source, { withFileTypes: true });
3909
+ for (const entry of entries) {
3910
+ const sourcePath = (0, import_path3.join)(source, entry.name);
3911
+ const targetPath = (0, import_path3.join)(target, entry.name);
3912
+ if (entry.isDirectory()) {
3913
+ copyDirectory2(sourcePath, targetPath);
3914
+ } else {
3915
+ (0, import_fs3.copyFileSync)(sourcePath, targetPath);
3916
+ }
3917
+ }
3918
+ }
3919
+
3920
+ // src/utils/setup/setup-mcp.ts
3921
+ var import_fs4 = require("fs");
3922
+ var import_path4 = require("path");
3721
3923
  function getAgentMCPTarget(agent) {
3722
3924
  switch (agent) {
3723
3925
  case "claude-code":
@@ -3731,18 +3933,18 @@ function getAgentMCPTarget(agent) {
3731
3933
  }
3732
3934
  async function setupMCPConfiguration(projectPath, mcpConfig, agent) {
3733
3935
  const target = getAgentMCPTarget(agent);
3734
- const mcpJsonPath = (0, import_path3.join)(projectPath, target.filePath);
3936
+ const mcpJsonPath = (0, import_path4.join)(projectPath, target.filePath);
3735
3937
  if (target.dir) {
3736
- const dir = (0, import_path3.join)(projectPath, target.dir);
3737
- if (!(0, import_fs3.existsSync)(dir)) {
3738
- (0, import_fs3.mkdirSync)(dir, { recursive: true });
3938
+ const dir = (0, import_path4.join)(projectPath, target.dir);
3939
+ if (!(0, import_fs4.existsSync)(dir)) {
3940
+ (0, import_fs4.mkdirSync)(dir, { recursive: true });
3739
3941
  }
3740
3942
  }
3741
3943
  const red2 = (text) => `\x1B[31m${text}\x1B[0m`;
3742
3944
  let existingFile = {};
3743
- if ((0, import_fs3.existsSync)(mcpJsonPath)) {
3945
+ if ((0, import_fs4.existsSync)(mcpJsonPath)) {
3744
3946
  try {
3745
- const content = (0, import_fs3.readFileSync)(mcpJsonPath, "utf-8");
3947
+ const content = (0, import_fs4.readFileSync)(mcpJsonPath, "utf-8");
3746
3948
  existingFile = JSON.parse(content);
3747
3949
  } catch {
3748
3950
  const box = [
@@ -3770,7 +3972,7 @@ async function setupMCPConfiguration(projectPath, mcpConfig, agent) {
3770
3972
  }
3771
3973
  const mergedServers = { ...existingServers, ...newServers };
3772
3974
  const outputFile = { ...existingFile, [target.rootKey]: mergedServers };
3773
- (0, import_fs3.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
3975
+ (0, import_fs4.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
3774
3976
  return { addedServers, skippedServers };
3775
3977
  }
3776
3978
  function buildMCPConfiguration(selectedItems, baseMcpServers, allMcpServers) {
@@ -3791,8 +3993,8 @@ function buildMCPConfiguration(selectedItems, baseMcpServers, allMcpServers) {
3791
3993
  }
3792
3994
 
3793
3995
  // src/utils/setup/setup-instructions.ts
3794
- var import_fs4 = require("fs");
3795
- var import_path4 = require("path");
3996
+ var import_fs5 = require("fs");
3997
+ var import_path5 = require("path");
3796
3998
  var MARKER_START = "<!-- one-shot-installer:start -->";
3797
3999
  var MARKER_END = "<!-- one-shot-installer:end -->";
3798
4000
  var red = (text) => `\x1B[31m${text}\x1B[0m`;
@@ -3800,11 +4002,11 @@ function upsertBlock(filePath, block) {
3800
4002
  const marked = `${MARKER_START}
3801
4003
  ${block}
3802
4004
  ${MARKER_END}`;
3803
- if (!(0, import_fs4.existsSync)(filePath)) {
3804
- (0, import_fs4.writeFileSync)(filePath, marked, "utf-8");
4005
+ if (!(0, import_fs5.existsSync)(filePath)) {
4006
+ (0, import_fs5.writeFileSync)(filePath, marked, "utf-8");
3805
4007
  return;
3806
4008
  }
3807
- const existing = (0, import_fs4.readFileSync)(filePath, "utf-8");
4009
+ const existing = (0, import_fs5.readFileSync)(filePath, "utf-8");
3808
4010
  const start = existing.indexOf(MARKER_START);
3809
4011
  const end = existing.indexOf(MARKER_END);
3810
4012
  const hasStart = start !== -1;
@@ -3829,23 +4031,23 @@ ${MARKER_END}`;
3829
4031
  }
3830
4032
  if (hasStart && hasEnd) {
3831
4033
  const updated = existing.slice(0, start) + marked + existing.slice(end + MARKER_END.length);
3832
- (0, import_fs4.writeFileSync)(filePath, updated, "utf-8");
4034
+ (0, import_fs5.writeFileSync)(filePath, updated, "utf-8");
3833
4035
  } else {
3834
4036
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
3835
- (0, import_fs4.writeFileSync)(filePath, existing + separator + marked, "utf-8");
4037
+ (0, import_fs5.writeFileSync)(filePath, existing + separator + marked, "utf-8");
3836
4038
  }
3837
4039
  }
3838
4040
  function collectMdFiles(dir) {
3839
- if (!(0, import_fs4.existsSync)(dir)) return [];
3840
- const entries = (0, import_fs4.readdirSync)(dir, { recursive: true });
4041
+ if (!(0, import_fs5.existsSync)(dir)) return [];
4042
+ const entries = (0, import_fs5.readdirSync)(dir, { recursive: true });
3841
4043
  return entries.filter((entry) => {
3842
- const fullPath = (0, import_path4.join)(dir, entry);
3843
- return entry.endsWith(".md") && (0, import_fs4.statSync)(fullPath).isFile();
4044
+ const fullPath = (0, import_path5.join)(dir, entry);
4045
+ return entry.endsWith(".md") && (0, import_fs5.statSync)(fullPath).isFile();
3844
4046
  }).map((entry) => entry.replace(/\\/g, "/")).sort();
3845
4047
  }
3846
4048
  function extractFirstHeading(filePath) {
3847
4049
  try {
3848
- const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
4050
+ const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
3849
4051
  const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, "");
3850
4052
  const match = withoutFrontmatter.match(/^#\s+(.+)/m);
3851
4053
  if (match) {
@@ -3879,18 +4081,18 @@ function buildMCPSection(mcpConfig) {
3879
4081
  return lines2.join("\n");
3880
4082
  }
3881
4083
  function resolveDomainFolder(domain, contextsDir) {
3882
- if ((0, import_fs4.existsSync)(contextsDir)) {
4084
+ if ((0, import_fs5.existsSync)(contextsDir)) {
3883
4085
  try {
3884
- const entries = (0, import_fs4.readdirSync)(contextsDir);
4086
+ const entries = (0, import_fs5.readdirSync)(contextsDir);
3885
4087
  const match = entries.find((e) => e.toLowerCase() === domain.toLowerCase());
3886
4088
  if (match) {
3887
- return { folderName: match, folderPath: (0, import_path4.join)(contextsDir, match) };
4089
+ return { folderName: match, folderPath: (0, import_path5.join)(contextsDir, match) };
3888
4090
  }
3889
4091
  } catch {
3890
4092
  }
3891
4093
  }
3892
4094
  const fallback = domain.toUpperCase();
3893
- return { folderName: fallback, folderPath: (0, import_path4.join)(contextsDir, fallback) };
4095
+ return { folderName: fallback, folderPath: (0, import_path5.join)(contextsDir, fallback) };
3894
4096
  }
3895
4097
  function formatPathRef(contextPath, description, agent) {
3896
4098
  if (agent === "github-copilot") {
@@ -3908,34 +4110,34 @@ function buildContextRefsSection(domains, agent, contextsDir) {
3908
4110
  let hasAnyFiles = false;
3909
4111
  for (const domain of domains) {
3910
4112
  const { folderName, folderPath: domainPath } = resolveDomainFolder(domain, contextsDir);
3911
- if (!(0, import_fs4.existsSync)(domainPath)) continue;
4113
+ if (!(0, import_fs5.existsSync)(domainPath)) continue;
3912
4114
  const domainFiles = [];
3913
- const ctxInstructions = (0, import_path4.join)(domainPath, "context-instructions.md");
3914
- if ((0, import_fs4.existsSync)(ctxInstructions)) {
4115
+ const ctxInstructions = (0, import_path5.join)(domainPath, "context-instructions.md");
4116
+ if ((0, import_fs5.existsSync)(ctxInstructions)) {
3915
4117
  const desc = extractFirstHeading(ctxInstructions);
3916
4118
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/context-instructions.md`, desc, agent));
3917
4119
  }
3918
- const instructionsMd = (0, import_path4.join)(domainPath, "core", "instructions.md");
3919
- if ((0, import_fs4.existsSync)(instructionsMd)) {
4120
+ const instructionsMd = (0, import_path5.join)(domainPath, "core", "instructions.md");
4121
+ if ((0, import_fs5.existsSync)(instructionsMd)) {
3920
4122
  const desc = extractFirstHeading(instructionsMd);
3921
4123
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/instructions.md`, `${desc} (start here)`, agent));
3922
4124
  }
3923
- const coreDir = (0, import_path4.join)(domainPath, "core");
3924
- if ((0, import_fs4.existsSync)(coreDir)) {
4125
+ const coreDir = (0, import_path5.join)(domainPath, "core");
4126
+ if ((0, import_fs5.existsSync)(coreDir)) {
3925
4127
  const coreFiles = collectMdFiles(coreDir).filter((f) => f !== "instructions.md" && !f.startsWith("instructions/"));
3926
4128
  for (const file of coreFiles) {
3927
- const desc = extractFirstHeading((0, import_path4.join)(coreDir, file));
4129
+ const desc = extractFirstHeading((0, import_path5.join)(coreDir, file));
3928
4130
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/${file}`, desc, agent));
3929
4131
  }
3930
4132
  }
3931
- const refDir = (0, import_path4.join)(domainPath, "reference");
3932
- if ((0, import_fs4.existsSync)(refDir)) {
4133
+ const refDir = (0, import_path5.join)(domainPath, "reference");
4134
+ if ((0, import_fs5.existsSync)(refDir)) {
3933
4135
  const refFiles = collectMdFiles(refDir);
3934
4136
  if (refFiles.length > 0) {
3935
4137
  domainFiles.push(``);
3936
4138
  domainFiles.push(`**Reference & cheat sheets:**`);
3937
4139
  for (const file of refFiles) {
3938
- const desc = extractFirstHeading((0, import_path4.join)(refDir, file));
4140
+ const desc = extractFirstHeading((0, import_path5.join)(refDir, file));
3939
4141
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/reference/${file}`, desc, agent));
3940
4142
  }
3941
4143
  }
@@ -3952,8 +4154,26 @@ function buildContextRefsSection(domains, agent, contextsDir) {
3952
4154
  }
3953
4155
  return lines2.join("\n");
3954
4156
  }
3955
- function buildCombinedInstructions(domains, mcpConfig, agent = "", projectPath = process.cwd()) {
3956
- const contextsDir = (0, import_path4.join)(projectPath, "_ai-context");
4157
+ function buildFactorySection() {
4158
+ return [
4159
+ `## Factory Dev Workflow`,
4160
+ ``,
4161
+ `Factory is installed. Use \`/factory:dev-plan\` to start or resume the workflow.`,
4162
+ ``,
4163
+ `Available commands:`,
4164
+ `- \`/factory:dev-plan\` \u2014 start or resume the workflow`,
4165
+ `- \`/factory:progress\` \u2014 check status`,
4166
+ `- \`/factory:analyst\` \u2014 validate a story`,
4167
+ `- \`/factory:architect\` \u2014 design architecture`,
4168
+ `- \`/factory:planner\` \u2014 create story plan`,
4169
+ `- \`/factory:verifier\` \u2014 code review`,
4170
+ `- \`/factory:security\` \u2014 OWASP security review`,
4171
+ `- \`/factory:test-analyst\` \u2014 test quality review`,
4172
+ ``
4173
+ ].join("\n");
4174
+ }
4175
+ function buildCombinedInstructions(domains, mcpConfig, agent = "", projectPath = process.cwd(), factoryInstalled = false) {
4176
+ const contextsDir = (0, import_path5.join)(projectPath, "_ai-context");
3957
4177
  const lines2 = [
3958
4178
  `# AI Development Instructions`,
3959
4179
  ``,
@@ -3964,44 +4184,47 @@ function buildCombinedInstructions(domains, mcpConfig, agent = "", projectPath =
3964
4184
  if (mcpConfig && Object.keys(mcpConfig).length > 0) {
3965
4185
  lines2.push(buildMCPSection(mcpConfig));
3966
4186
  }
4187
+ if (factoryInstalled) {
4188
+ lines2.push(buildFactorySection());
4189
+ }
3967
4190
  return lines2.join("\n");
3968
4191
  }
3969
4192
  async function setupInstructions(config) {
3970
- const { domains, agent, mcpConfig, projectPath = process.cwd() } = config;
4193
+ const { domains, agent, mcpConfig, projectPath = process.cwd(), factoryInstalled = false } = config;
3971
4194
  switch (agent) {
3972
4195
  case "claude-code": {
3973
- const content = buildCombinedInstructions(domains, mcpConfig, agent, projectPath);
3974
- upsertBlock((0, import_path4.join)(projectPath, "CLAUDE.md"), content);
4196
+ const content = buildCombinedInstructions(domains, mcpConfig, agent, projectPath, factoryInstalled);
4197
+ upsertBlock((0, import_path5.join)(projectPath, "CLAUDE.md"), content);
3975
4198
  break;
3976
4199
  }
3977
4200
  case "github-copilot": {
3978
- const agentsDir = (0, import_path4.join)(projectPath, ".github", "agents");
3979
- if (!(0, import_fs4.existsSync)(agentsDir)) (0, import_fs4.mkdirSync)(agentsDir, { recursive: true });
3980
- const filePath = (0, import_path4.join)(agentsDir, "instructions.md");
3981
- if (!(0, import_fs4.existsSync)(filePath)) {
3982
- (0, import_fs4.writeFileSync)(filePath, `---
4201
+ const agentsDir = (0, import_path5.join)(projectPath, ".github", "agents");
4202
+ if (!(0, import_fs5.existsSync)(agentsDir)) (0, import_fs5.mkdirSync)(agentsDir, { recursive: true });
4203
+ const filePath = (0, import_path5.join)(agentsDir, "instructions.md");
4204
+ if (!(0, import_fs5.existsSync)(filePath)) {
4205
+ (0, import_fs5.writeFileSync)(filePath, `---
3983
4206
  applyTo: "**"
3984
4207
  ---
3985
4208
 
3986
4209
  `, "utf-8");
3987
4210
  }
3988
- const body = buildCombinedInstructions(domains, mcpConfig, agent, projectPath);
4211
+ const body = buildCombinedInstructions(domains, mcpConfig, agent, projectPath, factoryInstalled);
3989
4212
  upsertBlock(filePath, body);
3990
4213
  break;
3991
4214
  }
3992
4215
  case "cursor": {
3993
- const cursorDir = (0, import_path4.join)(projectPath, ".cursor", "rules");
3994
- if (!(0, import_fs4.existsSync)(cursorDir)) (0, import_fs4.mkdirSync)(cursorDir, { recursive: true });
3995
- const filePath = (0, import_path4.join)(cursorDir, "instructions.mdc");
3996
- if (!(0, import_fs4.existsSync)(filePath)) {
3997
- (0, import_fs4.writeFileSync)(filePath, `---
4216
+ const cursorDir = (0, import_path5.join)(projectPath, ".cursor", "rules");
4217
+ if (!(0, import_fs5.existsSync)(cursorDir)) (0, import_fs5.mkdirSync)(cursorDir, { recursive: true });
4218
+ const filePath = (0, import_path5.join)(cursorDir, "instructions.mdc");
4219
+ if (!(0, import_fs5.existsSync)(filePath)) {
4220
+ (0, import_fs5.writeFileSync)(filePath, `---
3998
4221
  description: AI development instructions from One-Shot Installer
3999
4222
  alwaysApply: true
4000
4223
  ---
4001
4224
 
4002
4225
  `, "utf-8");
4003
4226
  }
4004
- const body = buildCombinedInstructions(domains, mcpConfig, agent, projectPath);
4227
+ const body = buildCombinedInstructions(domains, mcpConfig, agent, projectPath, factoryInstalled);
4005
4228
  upsertBlock(filePath, body);
4006
4229
  break;
4007
4230
  }
@@ -4011,8 +4234,8 @@ alwaysApply: true
4011
4234
  }
4012
4235
 
4013
4236
  // src/utils/setup/setup-gitignore.ts
4014
- var import_fs5 = require("fs");
4015
- var import_path5 = require("path");
4237
+ var import_fs6 = require("fs");
4238
+ var import_path6 = require("path");
4016
4239
  var MARKER_START2 = "# one-shot-installer:start";
4017
4240
  var MARKER_END2 = "# one-shot-installer:end";
4018
4241
  var GITIGNORE_ENTRIES = [
@@ -4027,24 +4250,37 @@ var GITIGNORE_ENTRIES = [
4027
4250
  ".claude/agents/",
4028
4251
  ".claude/agent-memory/",
4029
4252
  ".claude/plans/",
4030
- ".claude/settings.local.json"
4253
+ ".claude/settings.local.json",
4254
+ "",
4255
+ "# Factory runtime state (generated by workflow, not committed)",
4256
+ "factory/STATE.md",
4257
+ "factory/PROJECT.md",
4258
+ "factory/REQUIREMENTS.md",
4259
+ "factory/ARCHITECTURE.md",
4260
+ "factory/ROADMAP.md",
4261
+ "factory/PLAN.md",
4262
+ "factory/GATE-REPORT.md",
4263
+ "factory/VERIFY-REPORT.md",
4264
+ "factory/COMPLETION.md",
4265
+ "factory/summaries/",
4266
+ "factory/ai-context/"
4031
4267
  ];
4032
4268
  function setupGitignore(projectPath) {
4033
- const gitignorePath = (0, import_path5.join)(projectPath, ".gitignore");
4269
+ const gitignorePath = (0, import_path6.join)(projectPath, ".gitignore");
4034
4270
  const block = [MARKER_START2, ...GITIGNORE_ENTRIES, MARKER_END2].join("\n");
4035
- if (!(0, import_fs5.existsSync)(gitignorePath)) {
4036
- (0, import_fs5.writeFileSync)(gitignorePath, block + "\n", "utf-8");
4271
+ if (!(0, import_fs6.existsSync)(gitignorePath)) {
4272
+ (0, import_fs6.writeFileSync)(gitignorePath, block + "\n", "utf-8");
4037
4273
  return;
4038
4274
  }
4039
- const existing = (0, import_fs5.readFileSync)(gitignorePath, "utf-8");
4275
+ const existing = (0, import_fs6.readFileSync)(gitignorePath, "utf-8");
4040
4276
  const start = existing.indexOf(MARKER_START2);
4041
4277
  const end = existing.indexOf(MARKER_END2);
4042
4278
  if (start !== -1 && end !== -1 && end > start) {
4043
4279
  const updated = existing.slice(0, start) + block + existing.slice(end + MARKER_END2.length);
4044
- (0, import_fs5.writeFileSync)(gitignorePath, updated, "utf-8");
4280
+ (0, import_fs6.writeFileSync)(gitignorePath, updated, "utf-8");
4045
4281
  } else {
4046
4282
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
4047
- (0, import_fs5.writeFileSync)(gitignorePath, existing + separator + block + "\n", "utf-8");
4283
+ (0, import_fs6.writeFileSync)(gitignorePath, existing + separator + block + "\n", "utf-8");
4048
4284
  }
4049
4285
  }
4050
4286
 
@@ -4068,7 +4304,7 @@ async function loadWizardOptions(token, repo) {
4068
4304
  }
4069
4305
 
4070
4306
  // src/index.ts
4071
- var INSTALLER_VERSION = "0.4.11";
4307
+ var INSTALLER_VERSION = "0.5.7";
4072
4308
  function getInstructionFilePath(agent) {
4073
4309
  switch (agent) {
4074
4310
  case "claude-code":
@@ -4097,12 +4333,31 @@ function formatMCPCommand(server) {
4097
4333
  if (server.command) return `${server.command} ${(server.args ?? []).join(" ")}`.trimEnd();
4098
4334
  return "";
4099
4335
  }
4336
+ var dim = (text) => `\x1B[2m${text}\x1B[0m`;
4337
+ var yellow = (text) => `\x1B[33m${text}\x1B[0m`;
4338
+ var green = (text) => `\x1B[32m${text}\x1B[0m`;
4339
+ function detectMarkerFileMode(filePath, markerStart) {
4340
+ if (!(0, import_fs7.existsSync)(filePath)) return green("create");
4341
+ const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
4342
+ if (content.includes(markerStart)) return yellow("update");
4343
+ return yellow("append");
4344
+ }
4345
+ function detectMCPFileMode(filePath) {
4346
+ if (!(0, import_fs7.existsSync)(filePath)) return green("create");
4347
+ return yellow("merge");
4348
+ }
4349
+ function detectContextMode(projectPath, domain) {
4350
+ const contextDir = (0, import_path7.join)(projectPath, "_ai-context", domain.toUpperCase());
4351
+ const contextDirLower = (0, import_path7.join)(projectPath, "_ai-context", domain);
4352
+ if ((0, import_fs7.existsSync)(contextDir) || (0, import_fs7.existsSync)(contextDirLower)) return yellow("overwrite");
4353
+ return green("create");
4354
+ }
4100
4355
  function waitForEnter() {
4101
4356
  const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
4102
- return new Promise((resolve3) => {
4357
+ return new Promise((resolve4) => {
4103
4358
  rl.question(" Press Enter to close...", () => {
4104
4359
  rl.close();
4105
- resolve3();
4360
+ resolve4();
4106
4361
  });
4107
4362
  });
4108
4363
  }
@@ -4111,11 +4366,17 @@ async function main() {
4111
4366
  console.log("One-Shot Setup Installer\n");
4112
4367
  try {
4113
4368
  const token = await getGitHubToken();
4114
- console.log(" Locating data repository...");
4115
- const repo = await discoverRepo(token);
4369
+ console.log(" Locating repositories...");
4370
+ const [repo, factoryRepo] = await Promise.all([
4371
+ discoverRepo(token),
4372
+ discoverRepo(token, "factory-data").catch(() => null)
4373
+ ]);
4374
+ if (!factoryRepo) {
4375
+ console.log(" \u26A0 Factory repo not found \u2014 Factory will not be installed.");
4376
+ }
4116
4377
  console.log(" Loading configuration...");
4117
4378
  const options = await loadWizardOptions(token, repo);
4118
- const config = await collectInputs(options, options.releaseVersion);
4379
+ const config = await collectInputs(options, options.releaseVersion, !!factoryRepo);
4119
4380
  if (!config) {
4120
4381
  await waitForEnter();
4121
4382
  return;
@@ -4126,7 +4387,7 @@ async function main() {
4126
4387
  await waitForEnter();
4127
4388
  return;
4128
4389
  }
4129
- const result = await execute(config, token, repo);
4390
+ const result = await execute(config, token, repo, factoryRepo);
4130
4391
  printSummary(result);
4131
4392
  return;
4132
4393
  } catch (error) {
@@ -4134,7 +4395,7 @@ async function main() {
4134
4395
  await waitForEnter();
4135
4396
  }
4136
4397
  }
4137
- async function collectInputs(options, releaseVersion) {
4398
+ async function collectInputs(options, releaseVersion, factoryAvailable = false) {
4138
4399
  const selectedIds = await esm_default2({
4139
4400
  message: "Personas:",
4140
4401
  instructions: " Space to select \xB7 Enter to confirm",
@@ -4166,16 +4427,17 @@ async function collectInputs(options, releaseVersion) {
4166
4427
  }
4167
4428
  const projectInput = await esm_default4({
4168
4429
  message: "Project directory:",
4169
- default: (0, import_path6.resolve)(process.cwd())
4430
+ default: (0, import_path7.resolve)(process.cwd())
4170
4431
  });
4171
4432
  return {
4172
4433
  personas: selectedPersonas,
4173
4434
  agent,
4174
4435
  azureDevOpsOrg,
4175
- projectPath: (0, import_path6.resolve)(projectInput),
4436
+ projectPath: (0, import_path7.resolve)(projectInput),
4176
4437
  baseMcpServers: options.baseMcpServers,
4177
4438
  mcpConfig,
4178
- releaseVersion
4439
+ releaseVersion,
4440
+ installFactory: factoryAvailable
4179
4441
  };
4180
4442
  }
4181
4443
  async function previewAndConfirm(config, options) {
@@ -4184,21 +4446,41 @@ async function previewAndConfirm(config, options) {
4184
4446
  const instructionFile = getInstructionFilePath(config.agent);
4185
4447
  const mcpConfigFile = getMCPConfigPath(config.agent);
4186
4448
  const serverEntries = Object.entries(config.mcpConfig);
4449
+ const instructionFilePath = (0, import_path7.join)(config.projectPath, instructionFile);
4450
+ const mcpConfigFilePath = (0, import_path7.join)(config.projectPath, mcpConfigFile);
4451
+ const gitignorePath = (0, import_path7.join)(config.projectPath, ".gitignore");
4452
+ const instructionMode = detectMarkerFileMode(instructionFilePath, "<!-- one-shot-installer:start -->");
4453
+ const mcpMode = detectMCPFileMode(mcpConfigFilePath);
4454
+ const gitignoreMode = detectMarkerFileMode(gitignorePath, "# one-shot-installer:start");
4455
+ const uniqueDomains = [...new Set(config.personas.flatMap((p) => p.domains))];
4456
+ const domainModes = uniqueDomains.map((d) => ({
4457
+ domain: d,
4458
+ mode: detectContextMode(config.projectPath, d)
4459
+ }));
4187
4460
  console.log("\n" + "\u2500".repeat(48));
4188
4461
  console.log(" Ready to install");
4189
4462
  console.log("\u2500".repeat(48) + "\n");
4190
4463
  console.log(` Personas ${personaNames}`);
4191
4464
  console.log(` Tool ${agentDisplay}`);
4465
+ console.log(` Factory ${config.installFactory ? "yes" : "no"}`);
4192
4466
  console.log(` Directory ${config.projectPath}`);
4193
4467
  console.log("");
4194
- console.log(` Instruction file`);
4195
- console.log(` ${instructionFile}`);
4196
- console.log("");
4197
- console.log(` MCP config`);
4198
- console.log(` ${mcpConfigFile}`);
4468
+ console.log(` ${dim("File actions:")}`);
4469
+ const domainColWidth = Math.max(...domainModes.map((d) => d.domain.length), 6) + 2;
4470
+ for (const { domain, mode } of domainModes) {
4471
+ console.log(` _ai-context/${domain.padEnd(domainColWidth)}${mode}`);
4472
+ }
4473
+ console.log(` ${instructionFile.padEnd(domainColWidth + 14)}${instructionMode}`);
4474
+ console.log(` ${mcpConfigFile.padEnd(domainColWidth + 14)}${mcpMode}`);
4475
+ console.log(` ${".gitignore".padEnd(domainColWidth + 14)}${gitignoreMode}`);
4476
+ if (config.installFactory) {
4477
+ const factoryExists = (0, import_fs7.existsSync)((0, import_path7.join)(config.projectPath, "factory"));
4478
+ const factoryMode = factoryExists ? yellow("overwrite") : green("create");
4479
+ console.log(` ${"factory/".padEnd(domainColWidth + 14)}${factoryMode}`);
4480
+ }
4199
4481
  if (serverEntries.length > 0) {
4200
4482
  console.log("");
4201
- console.log(" MCP servers");
4483
+ console.log(` ${dim("MCP servers:")}`);
4202
4484
  const maxLen = Math.max(...serverEntries.map(([name]) => name.length));
4203
4485
  for (const [name, server] of serverEntries) {
4204
4486
  const cmd = formatMCPCommand(server);
@@ -4208,7 +4490,7 @@ async function previewAndConfirm(config, options) {
4208
4490
  console.log("\n" + "\u2500".repeat(48));
4209
4491
  return esm_default3({ message: "Proceed?", default: true });
4210
4492
  }
4211
- async function execute(config, token, repo) {
4493
+ async function execute(config, token, repo, factoryRepo = null) {
4212
4494
  const instructionFilePath = getInstructionFilePath(config.agent);
4213
4495
  const mcpConfigPath = getMCPConfigPath(config.agent);
4214
4496
  const result = {
@@ -4221,7 +4503,8 @@ async function execute(config, token, repo) {
4221
4503
  mcpConfigured: false,
4222
4504
  mcpConfigPath,
4223
4505
  mcpServersAdded: [],
4224
- gitignoreUpdated: false
4506
+ gitignoreUpdated: false,
4507
+ factoryInstalled: false
4225
4508
  };
4226
4509
  console.log("");
4227
4510
  const uniqueDomains = [...new Set(config.personas.flatMap((p) => p.domains))];
@@ -4245,6 +4528,22 @@ async function execute(config, token, repo) {
4245
4528
  console.log(` ${domain.padEnd(domainColWidth)}${status}`);
4246
4529
  }
4247
4530
  console.log("");
4531
+ if (config.installFactory && factoryRepo) {
4532
+ process.stdout.write(` Installing Factory framework... `);
4533
+ try {
4534
+ const factoryResult = await fetchFactory({ token, repo: factoryRepo, targetDir: config.projectPath });
4535
+ if (factoryResult.success) {
4536
+ result.factoryInstalled = true;
4537
+ console.log("\u2713");
4538
+ } else {
4539
+ console.log("\u2717");
4540
+ console.log(` ${factoryResult.failureReason}`);
4541
+ }
4542
+ } catch (error) {
4543
+ console.log("\u2717");
4544
+ console.log(` ${error instanceof Error ? error.message : String(error)}`);
4545
+ }
4546
+ }
4248
4547
  if (result.domainsInstalled.length === 0) {
4249
4548
  result.success = false;
4250
4549
  return result;
@@ -4260,7 +4559,7 @@ async function execute(config, token, repo) {
4260
4559
  const filteredMcpConfig = Object.fromEntries(
4261
4560
  Object.entries(config.mcpConfig).filter(([name]) => successfulMcpServers.has(name))
4262
4561
  );
4263
- const statePath = (0, import_path6.join)(config.projectPath, ".one-shot-state.json");
4562
+ const statePath = (0, import_path7.join)(config.projectPath, ".one-shot-state.json");
4264
4563
  const allDomains = result.domainsInstalled;
4265
4564
  process.stdout.write(` Writing ${instructionFilePath}... `);
4266
4565
  try {
@@ -4268,7 +4567,8 @@ async function execute(config, token, repo) {
4268
4567
  domains: allDomains,
4269
4568
  agent: config.agent,
4270
4569
  mcpConfig: filteredMcpConfig,
4271
- projectPath: config.projectPath
4570
+ projectPath: config.projectPath,
4571
+ factoryInstalled: result.factoryInstalled
4272
4572
  });
4273
4573
  result.instructionsCreated = true;
4274
4574
  console.log("\u2713");
@@ -4313,14 +4613,17 @@ async function execute(config, token, repo) {
4313
4613
  domains: allDomains,
4314
4614
  mcpServers: Object.keys(filteredMcpConfig),
4315
4615
  mcpConfigs: filteredMcpConfig,
4616
+ factoryInstalled: result.factoryInstalled,
4316
4617
  files: {
4317
4618
  instructions: instructionFilePath,
4318
4619
  mcpConfig: mcpConfigPath,
4319
- contexts: "_ai-context/"
4620
+ contexts: "_ai-context/",
4621
+ factory: result.factoryInstalled ? "factory/" : null,
4622
+ globalConfig: (0, import_path7.join)((0, import_os2.homedir)(), ".one-shot-installer")
4320
4623
  }
4321
4624
  };
4322
4625
  try {
4323
- (0, import_fs6.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4626
+ (0, import_fs7.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4324
4627
  } catch {
4325
4628
  }
4326
4629
  return result;
@@ -4339,6 +4642,9 @@ function printSummary(result) {
4339
4642
  const serverList = result.mcpServersAdded.length > 0 ? ` (${result.mcpServersAdded.join(", ")})` : "";
4340
4643
  console.log(` ${result.mcpConfigPath} written${serverList}`);
4341
4644
  }
4645
+ if (result.factoryInstalled) {
4646
+ console.log(` Factory installed (.claude/ + factory/)`);
4647
+ }
4342
4648
  if (result.gitignoreUpdated) {
4343
4649
  console.log(` .gitignore updated`);
4344
4650
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dlw-machine-setup",
3
- "version": "0.4.11",
3
+ "version": "0.5.7",
4
4
  "description": "One-shot installer for The Machine toolchain",
5
5
  "bin": {
6
6
  "dlw-machine-setup": "bin/installer.js"