dlw-machine-setup 0.4.11 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +27 -84
  2. package/bin/installer.js +350 -98
  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,194 @@ 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)(claudeDir, "agents"));
3805
+ copyDirectory2(agentsSrc, (0, import_path3.join)(factoryDir, "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.js" }]
3851
+ };
3852
+ const contextMonitorHook = {
3853
+ hooks: [{ type: "command", command: "node .claude/hooks/factory-context-monitor.js" }]
3854
+ };
3855
+ const stateGuardHook = {
3856
+ hooks: [{ type: "command", command: "node .claude/hooks/factory-state-guard.js" }]
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.js" };
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 copyDirectory2(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
+ const sourcePath = (0, import_path3.join)(source, entry.name);
3895
+ const targetPath = (0, import_path3.join)(target, entry.name);
3896
+ if (entry.isDirectory()) {
3897
+ copyDirectory2(sourcePath, targetPath);
3898
+ } else {
3899
+ (0, import_fs3.copyFileSync)(sourcePath, targetPath);
3900
+ }
3901
+ }
3902
+ }
3903
+
3904
+ // src/utils/setup/setup-mcp.ts
3905
+ var import_fs4 = require("fs");
3906
+ var import_path4 = require("path");
3721
3907
  function getAgentMCPTarget(agent) {
3722
3908
  switch (agent) {
3723
3909
  case "claude-code":
@@ -3731,18 +3917,18 @@ function getAgentMCPTarget(agent) {
3731
3917
  }
3732
3918
  async function setupMCPConfiguration(projectPath, mcpConfig, agent) {
3733
3919
  const target = getAgentMCPTarget(agent);
3734
- const mcpJsonPath = (0, import_path3.join)(projectPath, target.filePath);
3920
+ const mcpJsonPath = (0, import_path4.join)(projectPath, target.filePath);
3735
3921
  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 });
3922
+ const dir = (0, import_path4.join)(projectPath, target.dir);
3923
+ if (!(0, import_fs4.existsSync)(dir)) {
3924
+ (0, import_fs4.mkdirSync)(dir, { recursive: true });
3739
3925
  }
3740
3926
  }
3741
3927
  const red2 = (text) => `\x1B[31m${text}\x1B[0m`;
3742
3928
  let existingFile = {};
3743
- if ((0, import_fs3.existsSync)(mcpJsonPath)) {
3929
+ if ((0, import_fs4.existsSync)(mcpJsonPath)) {
3744
3930
  try {
3745
- const content = (0, import_fs3.readFileSync)(mcpJsonPath, "utf-8");
3931
+ const content = (0, import_fs4.readFileSync)(mcpJsonPath, "utf-8");
3746
3932
  existingFile = JSON.parse(content);
3747
3933
  } catch {
3748
3934
  const box = [
@@ -3770,7 +3956,7 @@ async function setupMCPConfiguration(projectPath, mcpConfig, agent) {
3770
3956
  }
3771
3957
  const mergedServers = { ...existingServers, ...newServers };
3772
3958
  const outputFile = { ...existingFile, [target.rootKey]: mergedServers };
3773
- (0, import_fs3.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
3959
+ (0, import_fs4.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
3774
3960
  return { addedServers, skippedServers };
3775
3961
  }
3776
3962
  function buildMCPConfiguration(selectedItems, baseMcpServers, allMcpServers) {
@@ -3791,8 +3977,8 @@ function buildMCPConfiguration(selectedItems, baseMcpServers, allMcpServers) {
3791
3977
  }
3792
3978
 
3793
3979
  // src/utils/setup/setup-instructions.ts
3794
- var import_fs4 = require("fs");
3795
- var import_path4 = require("path");
3980
+ var import_fs5 = require("fs");
3981
+ var import_path5 = require("path");
3796
3982
  var MARKER_START = "<!-- one-shot-installer:start -->";
3797
3983
  var MARKER_END = "<!-- one-shot-installer:end -->";
3798
3984
  var red = (text) => `\x1B[31m${text}\x1B[0m`;
@@ -3800,11 +3986,11 @@ function upsertBlock(filePath, block) {
3800
3986
  const marked = `${MARKER_START}
3801
3987
  ${block}
3802
3988
  ${MARKER_END}`;
3803
- if (!(0, import_fs4.existsSync)(filePath)) {
3804
- (0, import_fs4.writeFileSync)(filePath, marked, "utf-8");
3989
+ if (!(0, import_fs5.existsSync)(filePath)) {
3990
+ (0, import_fs5.writeFileSync)(filePath, marked, "utf-8");
3805
3991
  return;
3806
3992
  }
3807
- const existing = (0, import_fs4.readFileSync)(filePath, "utf-8");
3993
+ const existing = (0, import_fs5.readFileSync)(filePath, "utf-8");
3808
3994
  const start = existing.indexOf(MARKER_START);
3809
3995
  const end = existing.indexOf(MARKER_END);
3810
3996
  const hasStart = start !== -1;
@@ -3829,23 +4015,23 @@ ${MARKER_END}`;
3829
4015
  }
3830
4016
  if (hasStart && hasEnd) {
3831
4017
  const updated = existing.slice(0, start) + marked + existing.slice(end + MARKER_END.length);
3832
- (0, import_fs4.writeFileSync)(filePath, updated, "utf-8");
4018
+ (0, import_fs5.writeFileSync)(filePath, updated, "utf-8");
3833
4019
  } else {
3834
4020
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
3835
- (0, import_fs4.writeFileSync)(filePath, existing + separator + marked, "utf-8");
4021
+ (0, import_fs5.writeFileSync)(filePath, existing + separator + marked, "utf-8");
3836
4022
  }
3837
4023
  }
3838
4024
  function collectMdFiles(dir) {
3839
- if (!(0, import_fs4.existsSync)(dir)) return [];
3840
- const entries = (0, import_fs4.readdirSync)(dir, { recursive: true });
4025
+ if (!(0, import_fs5.existsSync)(dir)) return [];
4026
+ const entries = (0, import_fs5.readdirSync)(dir, { recursive: true });
3841
4027
  return entries.filter((entry) => {
3842
- const fullPath = (0, import_path4.join)(dir, entry);
3843
- return entry.endsWith(".md") && (0, import_fs4.statSync)(fullPath).isFile();
4028
+ const fullPath = (0, import_path5.join)(dir, entry);
4029
+ return entry.endsWith(".md") && (0, import_fs5.statSync)(fullPath).isFile();
3844
4030
  }).map((entry) => entry.replace(/\\/g, "/")).sort();
3845
4031
  }
3846
4032
  function extractFirstHeading(filePath) {
3847
4033
  try {
3848
- const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
4034
+ const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
3849
4035
  const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, "");
3850
4036
  const match = withoutFrontmatter.match(/^#\s+(.+)/m);
3851
4037
  if (match) {
@@ -3879,18 +4065,18 @@ function buildMCPSection(mcpConfig) {
3879
4065
  return lines2.join("\n");
3880
4066
  }
3881
4067
  function resolveDomainFolder(domain, contextsDir) {
3882
- if ((0, import_fs4.existsSync)(contextsDir)) {
4068
+ if ((0, import_fs5.existsSync)(contextsDir)) {
3883
4069
  try {
3884
- const entries = (0, import_fs4.readdirSync)(contextsDir);
4070
+ const entries = (0, import_fs5.readdirSync)(contextsDir);
3885
4071
  const match = entries.find((e) => e.toLowerCase() === domain.toLowerCase());
3886
4072
  if (match) {
3887
- return { folderName: match, folderPath: (0, import_path4.join)(contextsDir, match) };
4073
+ return { folderName: match, folderPath: (0, import_path5.join)(contextsDir, match) };
3888
4074
  }
3889
4075
  } catch {
3890
4076
  }
3891
4077
  }
3892
4078
  const fallback = domain.toUpperCase();
3893
- return { folderName: fallback, folderPath: (0, import_path4.join)(contextsDir, fallback) };
4079
+ return { folderName: fallback, folderPath: (0, import_path5.join)(contextsDir, fallback) };
3894
4080
  }
3895
4081
  function formatPathRef(contextPath, description, agent) {
3896
4082
  if (agent === "github-copilot") {
@@ -3908,34 +4094,34 @@ function buildContextRefsSection(domains, agent, contextsDir) {
3908
4094
  let hasAnyFiles = false;
3909
4095
  for (const domain of domains) {
3910
4096
  const { folderName, folderPath: domainPath } = resolveDomainFolder(domain, contextsDir);
3911
- if (!(0, import_fs4.existsSync)(domainPath)) continue;
4097
+ if (!(0, import_fs5.existsSync)(domainPath)) continue;
3912
4098
  const domainFiles = [];
3913
- const ctxInstructions = (0, import_path4.join)(domainPath, "context-instructions.md");
3914
- if ((0, import_fs4.existsSync)(ctxInstructions)) {
4099
+ const ctxInstructions = (0, import_path5.join)(domainPath, "context-instructions.md");
4100
+ if ((0, import_fs5.existsSync)(ctxInstructions)) {
3915
4101
  const desc = extractFirstHeading(ctxInstructions);
3916
4102
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/context-instructions.md`, desc, agent));
3917
4103
  }
3918
- const instructionsMd = (0, import_path4.join)(domainPath, "core", "instructions.md");
3919
- if ((0, import_fs4.existsSync)(instructionsMd)) {
4104
+ const instructionsMd = (0, import_path5.join)(domainPath, "core", "instructions.md");
4105
+ if ((0, import_fs5.existsSync)(instructionsMd)) {
3920
4106
  const desc = extractFirstHeading(instructionsMd);
3921
4107
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/instructions.md`, `${desc} (start here)`, agent));
3922
4108
  }
3923
- const coreDir = (0, import_path4.join)(domainPath, "core");
3924
- if ((0, import_fs4.existsSync)(coreDir)) {
4109
+ const coreDir = (0, import_path5.join)(domainPath, "core");
4110
+ if ((0, import_fs5.existsSync)(coreDir)) {
3925
4111
  const coreFiles = collectMdFiles(coreDir).filter((f) => f !== "instructions.md" && !f.startsWith("instructions/"));
3926
4112
  for (const file of coreFiles) {
3927
- const desc = extractFirstHeading((0, import_path4.join)(coreDir, file));
4113
+ const desc = extractFirstHeading((0, import_path5.join)(coreDir, file));
3928
4114
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/${file}`, desc, agent));
3929
4115
  }
3930
4116
  }
3931
- const refDir = (0, import_path4.join)(domainPath, "reference");
3932
- if ((0, import_fs4.existsSync)(refDir)) {
4117
+ const refDir = (0, import_path5.join)(domainPath, "reference");
4118
+ if ((0, import_fs5.existsSync)(refDir)) {
3933
4119
  const refFiles = collectMdFiles(refDir);
3934
4120
  if (refFiles.length > 0) {
3935
4121
  domainFiles.push(``);
3936
4122
  domainFiles.push(`**Reference & cheat sheets:**`);
3937
4123
  for (const file of refFiles) {
3938
- const desc = extractFirstHeading((0, import_path4.join)(refDir, file));
4124
+ const desc = extractFirstHeading((0, import_path5.join)(refDir, file));
3939
4125
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/reference/${file}`, desc, agent));
3940
4126
  }
3941
4127
  }
@@ -3952,8 +4138,26 @@ function buildContextRefsSection(domains, agent, contextsDir) {
3952
4138
  }
3953
4139
  return lines2.join("\n");
3954
4140
  }
3955
- function buildCombinedInstructions(domains, mcpConfig, agent = "", projectPath = process.cwd()) {
3956
- const contextsDir = (0, import_path4.join)(projectPath, "_ai-context");
4141
+ function buildFactorySection() {
4142
+ return [
4143
+ `## Factory Dev Workflow`,
4144
+ ``,
4145
+ `Factory is installed. Use \`/factory:dev-plan\` to start or resume the workflow.`,
4146
+ ``,
4147
+ `Available commands:`,
4148
+ `- \`/factory:dev-plan\` \u2014 start or resume the workflow`,
4149
+ `- \`/factory:progress\` \u2014 check status`,
4150
+ `- \`/factory:analyst\` \u2014 validate a story`,
4151
+ `- \`/factory:architect\` \u2014 design architecture`,
4152
+ `- \`/factory:planner\` \u2014 create story plan`,
4153
+ `- \`/factory:verifier\` \u2014 code review`,
4154
+ `- \`/factory:security\` \u2014 OWASP security review`,
4155
+ `- \`/factory:test-analyst\` \u2014 test quality review`,
4156
+ ``
4157
+ ].join("\n");
4158
+ }
4159
+ function buildCombinedInstructions(domains, mcpConfig, agent = "", projectPath = process.cwd(), factoryInstalled = false) {
4160
+ const contextsDir = (0, import_path5.join)(projectPath, "_ai-context");
3957
4161
  const lines2 = [
3958
4162
  `# AI Development Instructions`,
3959
4163
  ``,
@@ -3964,44 +4168,47 @@ function buildCombinedInstructions(domains, mcpConfig, agent = "", projectPath =
3964
4168
  if (mcpConfig && Object.keys(mcpConfig).length > 0) {
3965
4169
  lines2.push(buildMCPSection(mcpConfig));
3966
4170
  }
4171
+ if (factoryInstalled) {
4172
+ lines2.push(buildFactorySection());
4173
+ }
3967
4174
  return lines2.join("\n");
3968
4175
  }
3969
4176
  async function setupInstructions(config) {
3970
- const { domains, agent, mcpConfig, projectPath = process.cwd() } = config;
4177
+ const { domains, agent, mcpConfig, projectPath = process.cwd(), factoryInstalled = false } = config;
3971
4178
  switch (agent) {
3972
4179
  case "claude-code": {
3973
- const content = buildCombinedInstructions(domains, mcpConfig, agent, projectPath);
3974
- upsertBlock((0, import_path4.join)(projectPath, "CLAUDE.md"), content);
4180
+ const content = buildCombinedInstructions(domains, mcpConfig, agent, projectPath, factoryInstalled);
4181
+ upsertBlock((0, import_path5.join)(projectPath, "CLAUDE.md"), content);
3975
4182
  break;
3976
4183
  }
3977
4184
  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, `---
4185
+ const agentsDir = (0, import_path5.join)(projectPath, ".github", "agents");
4186
+ if (!(0, import_fs5.existsSync)(agentsDir)) (0, import_fs5.mkdirSync)(agentsDir, { recursive: true });
4187
+ const filePath = (0, import_path5.join)(agentsDir, "instructions.md");
4188
+ if (!(0, import_fs5.existsSync)(filePath)) {
4189
+ (0, import_fs5.writeFileSync)(filePath, `---
3983
4190
  applyTo: "**"
3984
4191
  ---
3985
4192
 
3986
4193
  `, "utf-8");
3987
4194
  }
3988
- const body = buildCombinedInstructions(domains, mcpConfig, agent, projectPath);
4195
+ const body = buildCombinedInstructions(domains, mcpConfig, agent, projectPath, factoryInstalled);
3989
4196
  upsertBlock(filePath, body);
3990
4197
  break;
3991
4198
  }
3992
4199
  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, `---
4200
+ const cursorDir = (0, import_path5.join)(projectPath, ".cursor", "rules");
4201
+ if (!(0, import_fs5.existsSync)(cursorDir)) (0, import_fs5.mkdirSync)(cursorDir, { recursive: true });
4202
+ const filePath = (0, import_path5.join)(cursorDir, "instructions.mdc");
4203
+ if (!(0, import_fs5.existsSync)(filePath)) {
4204
+ (0, import_fs5.writeFileSync)(filePath, `---
3998
4205
  description: AI development instructions from One-Shot Installer
3999
4206
  alwaysApply: true
4000
4207
  ---
4001
4208
 
4002
4209
  `, "utf-8");
4003
4210
  }
4004
- const body = buildCombinedInstructions(domains, mcpConfig, agent, projectPath);
4211
+ const body = buildCombinedInstructions(domains, mcpConfig, agent, projectPath, factoryInstalled);
4005
4212
  upsertBlock(filePath, body);
4006
4213
  break;
4007
4214
  }
@@ -4011,8 +4218,8 @@ alwaysApply: true
4011
4218
  }
4012
4219
 
4013
4220
  // src/utils/setup/setup-gitignore.ts
4014
- var import_fs5 = require("fs");
4015
- var import_path5 = require("path");
4221
+ var import_fs6 = require("fs");
4222
+ var import_path6 = require("path");
4016
4223
  var MARKER_START2 = "# one-shot-installer:start";
4017
4224
  var MARKER_END2 = "# one-shot-installer:end";
4018
4225
  var GITIGNORE_ENTRIES = [
@@ -4027,24 +4234,37 @@ var GITIGNORE_ENTRIES = [
4027
4234
  ".claude/agents/",
4028
4235
  ".claude/agent-memory/",
4029
4236
  ".claude/plans/",
4030
- ".claude/settings.local.json"
4237
+ ".claude/settings.local.json",
4238
+ "",
4239
+ "# Factory runtime state (generated by workflow, not committed)",
4240
+ "factory/STATE.md",
4241
+ "factory/PROJECT.md",
4242
+ "factory/REQUIREMENTS.md",
4243
+ "factory/ARCHITECTURE.md",
4244
+ "factory/ROADMAP.md",
4245
+ "factory/PLAN.md",
4246
+ "factory/GATE-REPORT.md",
4247
+ "factory/VERIFY-REPORT.md",
4248
+ "factory/COMPLETION.md",
4249
+ "factory/summaries/",
4250
+ "factory/ai-context/"
4031
4251
  ];
4032
4252
  function setupGitignore(projectPath) {
4033
- const gitignorePath = (0, import_path5.join)(projectPath, ".gitignore");
4253
+ const gitignorePath = (0, import_path6.join)(projectPath, ".gitignore");
4034
4254
  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");
4255
+ if (!(0, import_fs6.existsSync)(gitignorePath)) {
4256
+ (0, import_fs6.writeFileSync)(gitignorePath, block + "\n", "utf-8");
4037
4257
  return;
4038
4258
  }
4039
- const existing = (0, import_fs5.readFileSync)(gitignorePath, "utf-8");
4259
+ const existing = (0, import_fs6.readFileSync)(gitignorePath, "utf-8");
4040
4260
  const start = existing.indexOf(MARKER_START2);
4041
4261
  const end = existing.indexOf(MARKER_END2);
4042
4262
  if (start !== -1 && end !== -1 && end > start) {
4043
4263
  const updated = existing.slice(0, start) + block + existing.slice(end + MARKER_END2.length);
4044
- (0, import_fs5.writeFileSync)(gitignorePath, updated, "utf-8");
4264
+ (0, import_fs6.writeFileSync)(gitignorePath, updated, "utf-8");
4045
4265
  } else {
4046
4266
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
4047
- (0, import_fs5.writeFileSync)(gitignorePath, existing + separator + block + "\n", "utf-8");
4267
+ (0, import_fs6.writeFileSync)(gitignorePath, existing + separator + block + "\n", "utf-8");
4048
4268
  }
4049
4269
  }
4050
4270
 
@@ -4068,7 +4288,7 @@ async function loadWizardOptions(token, repo) {
4068
4288
  }
4069
4289
 
4070
4290
  // src/index.ts
4071
- var INSTALLER_VERSION = "0.4.11";
4291
+ var INSTALLER_VERSION = "0.5.0";
4072
4292
  function getInstructionFilePath(agent) {
4073
4293
  switch (agent) {
4074
4294
  case "claude-code":
@@ -4099,10 +4319,10 @@ function formatMCPCommand(server) {
4099
4319
  }
4100
4320
  function waitForEnter() {
4101
4321
  const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
4102
- return new Promise((resolve3) => {
4322
+ return new Promise((resolve4) => {
4103
4323
  rl.question(" Press Enter to close...", () => {
4104
4324
  rl.close();
4105
- resolve3();
4325
+ resolve4();
4106
4326
  });
4107
4327
  });
4108
4328
  }
@@ -4111,11 +4331,17 @@ async function main() {
4111
4331
  console.log("One-Shot Setup Installer\n");
4112
4332
  try {
4113
4333
  const token = await getGitHubToken();
4114
- console.log(" Locating data repository...");
4115
- const repo = await discoverRepo(token);
4334
+ console.log(" Locating repositories...");
4335
+ const [repo, factoryRepo] = await Promise.all([
4336
+ discoverRepo(token),
4337
+ discoverRepo(token, "factory-data").catch(() => null)
4338
+ ]);
4339
+ if (!factoryRepo) {
4340
+ console.log(" \u26A0 Factory repo not found \u2014 Factory will not be installed.");
4341
+ }
4116
4342
  console.log(" Loading configuration...");
4117
4343
  const options = await loadWizardOptions(token, repo);
4118
- const config = await collectInputs(options, options.releaseVersion);
4344
+ const config = await collectInputs(options, options.releaseVersion, !!factoryRepo);
4119
4345
  if (!config) {
4120
4346
  await waitForEnter();
4121
4347
  return;
@@ -4126,7 +4352,7 @@ async function main() {
4126
4352
  await waitForEnter();
4127
4353
  return;
4128
4354
  }
4129
- const result = await execute(config, token, repo);
4355
+ const result = await execute(config, token, repo, factoryRepo);
4130
4356
  printSummary(result);
4131
4357
  return;
4132
4358
  } catch (error) {
@@ -4134,7 +4360,7 @@ async function main() {
4134
4360
  await waitForEnter();
4135
4361
  }
4136
4362
  }
4137
- async function collectInputs(options, releaseVersion) {
4363
+ async function collectInputs(options, releaseVersion, factoryAvailable = false) {
4138
4364
  const selectedIds = await esm_default2({
4139
4365
  message: "Personas:",
4140
4366
  instructions: " Space to select \xB7 Enter to confirm",
@@ -4166,16 +4392,17 @@ async function collectInputs(options, releaseVersion) {
4166
4392
  }
4167
4393
  const projectInput = await esm_default4({
4168
4394
  message: "Project directory:",
4169
- default: (0, import_path6.resolve)(process.cwd())
4395
+ default: (0, import_path7.resolve)(process.cwd())
4170
4396
  });
4171
4397
  return {
4172
4398
  personas: selectedPersonas,
4173
4399
  agent,
4174
4400
  azureDevOpsOrg,
4175
- projectPath: (0, import_path6.resolve)(projectInput),
4401
+ projectPath: (0, import_path7.resolve)(projectInput),
4176
4402
  baseMcpServers: options.baseMcpServers,
4177
4403
  mcpConfig,
4178
- releaseVersion
4404
+ releaseVersion,
4405
+ installFactory: factoryAvailable
4179
4406
  };
4180
4407
  }
4181
4408
  async function previewAndConfirm(config, options) {
@@ -4189,6 +4416,7 @@ async function previewAndConfirm(config, options) {
4189
4416
  console.log("\u2500".repeat(48) + "\n");
4190
4417
  console.log(` Personas ${personaNames}`);
4191
4418
  console.log(` Tool ${agentDisplay}`);
4419
+ console.log(` Factory ${config.installFactory ? "yes" : "no"}`);
4192
4420
  console.log(` Directory ${config.projectPath}`);
4193
4421
  console.log("");
4194
4422
  console.log(` Instruction file`);
@@ -4208,7 +4436,7 @@ async function previewAndConfirm(config, options) {
4208
4436
  console.log("\n" + "\u2500".repeat(48));
4209
4437
  return esm_default3({ message: "Proceed?", default: true });
4210
4438
  }
4211
- async function execute(config, token, repo) {
4439
+ async function execute(config, token, repo, factoryRepo = null) {
4212
4440
  const instructionFilePath = getInstructionFilePath(config.agent);
4213
4441
  const mcpConfigPath = getMCPConfigPath(config.agent);
4214
4442
  const result = {
@@ -4221,7 +4449,8 @@ async function execute(config, token, repo) {
4221
4449
  mcpConfigured: false,
4222
4450
  mcpConfigPath,
4223
4451
  mcpServersAdded: [],
4224
- gitignoreUpdated: false
4452
+ gitignoreUpdated: false,
4453
+ factoryInstalled: false
4225
4454
  };
4226
4455
  console.log("");
4227
4456
  const uniqueDomains = [...new Set(config.personas.flatMap((p) => p.domains))];
@@ -4245,6 +4474,22 @@ async function execute(config, token, repo) {
4245
4474
  console.log(` ${domain.padEnd(domainColWidth)}${status}`);
4246
4475
  }
4247
4476
  console.log("");
4477
+ if (config.installFactory && factoryRepo) {
4478
+ process.stdout.write(` Installing Factory framework... `);
4479
+ try {
4480
+ const factoryResult = await fetchFactory({ token, repo: factoryRepo, targetDir: config.projectPath });
4481
+ if (factoryResult.success) {
4482
+ result.factoryInstalled = true;
4483
+ console.log("\u2713");
4484
+ } else {
4485
+ console.log("\u2717");
4486
+ console.log(` ${factoryResult.failureReason}`);
4487
+ }
4488
+ } catch (error) {
4489
+ console.log("\u2717");
4490
+ console.log(` ${error instanceof Error ? error.message : String(error)}`);
4491
+ }
4492
+ }
4248
4493
  if (result.domainsInstalled.length === 0) {
4249
4494
  result.success = false;
4250
4495
  return result;
@@ -4260,7 +4505,7 @@ async function execute(config, token, repo) {
4260
4505
  const filteredMcpConfig = Object.fromEntries(
4261
4506
  Object.entries(config.mcpConfig).filter(([name]) => successfulMcpServers.has(name))
4262
4507
  );
4263
- const statePath = (0, import_path6.join)(config.projectPath, ".one-shot-state.json");
4508
+ const statePath = (0, import_path7.join)(config.projectPath, ".one-shot-state.json");
4264
4509
  const allDomains = result.domainsInstalled;
4265
4510
  process.stdout.write(` Writing ${instructionFilePath}... `);
4266
4511
  try {
@@ -4268,7 +4513,8 @@ async function execute(config, token, repo) {
4268
4513
  domains: allDomains,
4269
4514
  agent: config.agent,
4270
4515
  mcpConfig: filteredMcpConfig,
4271
- projectPath: config.projectPath
4516
+ projectPath: config.projectPath,
4517
+ factoryInstalled: result.factoryInstalled
4272
4518
  });
4273
4519
  result.instructionsCreated = true;
4274
4520
  console.log("\u2713");
@@ -4313,14 +4559,17 @@ async function execute(config, token, repo) {
4313
4559
  domains: allDomains,
4314
4560
  mcpServers: Object.keys(filteredMcpConfig),
4315
4561
  mcpConfigs: filteredMcpConfig,
4562
+ factoryInstalled: result.factoryInstalled,
4316
4563
  files: {
4317
4564
  instructions: instructionFilePath,
4318
4565
  mcpConfig: mcpConfigPath,
4319
- contexts: "_ai-context/"
4566
+ contexts: "_ai-context/",
4567
+ factory: result.factoryInstalled ? "factory/" : null,
4568
+ globalConfig: (0, import_path7.join)((0, import_os2.homedir)(), ".one-shot-installer")
4320
4569
  }
4321
4570
  };
4322
4571
  try {
4323
- (0, import_fs6.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4572
+ (0, import_fs7.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4324
4573
  } catch {
4325
4574
  }
4326
4575
  return result;
@@ -4339,6 +4588,9 @@ function printSummary(result) {
4339
4588
  const serverList = result.mcpServersAdded.length > 0 ? ` (${result.mcpServersAdded.join(", ")})` : "";
4340
4589
  console.log(` ${result.mcpConfigPath} written${serverList}`);
4341
4590
  }
4591
+ if (result.factoryInstalled) {
4592
+ console.log(` Factory installed (.claude/ + factory/)`);
4593
+ }
4342
4594
  if (result.gitignoreUpdated) {
4343
4595
  console.log(` .gitignore updated`);
4344
4596
  }
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.0",
4
4
  "description": "One-shot installer for The Machine toolchain",
5
5
  "bin": {
6
6
  "dlw-machine-setup": "bin/installer.js"