dlw-machine-setup 0.4.10 → 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.
- package/README.md +27 -84
- package/bin/installer.js +392 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,108 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dlw-machine-setup
|
|
2
2
|
|
|
3
|
-
NPM wrapper for the One-Shot Setup installer. This package
|
|
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
|
-
##
|
|
6
|
-
|
|
7
|
-
### Using npx (Recommended)
|
|
8
|
-
|
|
9
|
-
No installation required - run directly:
|
|
5
|
+
## Usage
|
|
10
6
|
|
|
11
7
|
```bash
|
|
12
|
-
npx
|
|
8
|
+
npx dlw-machine-setup@latest
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
|
|
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
|
-
|
|
13
|
+
## What It Does
|
|
23
14
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
2. **Subsequent runs**: Your token is cached at `~/.one-shot-installer/github-token.json`
|
|
25
|
+
## Authentication
|
|
35
26
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
2752
|
+
let resolve4;
|
|
2753
2753
|
let reject;
|
|
2754
2754
|
const promise = new Promise((res, rej) => {
|
|
2755
|
-
|
|
2755
|
+
resolve4 = res;
|
|
2756
2756
|
reject = rej;
|
|
2757
2757
|
});
|
|
2758
|
-
return { promise, resolve:
|
|
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:
|
|
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(() =>
|
|
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
|
|
3243
|
+
var import_fs7 = require("fs");
|
|
3244
3244
|
var import_readline = require("readline");
|
|
3245
|
-
var
|
|
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((
|
|
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
|
|
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
|
-
|
|
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:${
|
|
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(
|
|
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(
|
|
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(
|
|
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((
|
|
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/
|
|
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,
|
|
3920
|
+
const mcpJsonPath = (0, import_path4.join)(projectPath, target.filePath);
|
|
3735
3921
|
if (target.dir) {
|
|
3736
|
-
const dir = (0,
|
|
3737
|
-
if (!(0,
|
|
3738
|
-
(0,
|
|
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,
|
|
3929
|
+
if ((0, import_fs4.existsSync)(mcpJsonPath)) {
|
|
3744
3930
|
try {
|
|
3745
|
-
const content = (0,
|
|
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,
|
|
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
|
|
3795
|
-
var
|
|
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,
|
|
3804
|
-
(0,
|
|
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,
|
|
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,
|
|
4018
|
+
(0, import_fs5.writeFileSync)(filePath, updated, "utf-8");
|
|
3833
4019
|
} else {
|
|
3834
4020
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
3835
|
-
(0,
|
|
4021
|
+
(0, import_fs5.writeFileSync)(filePath, existing + separator + marked, "utf-8");
|
|
3836
4022
|
}
|
|
3837
4023
|
}
|
|
3838
4024
|
function collectMdFiles(dir) {
|
|
3839
|
-
if (!(0,
|
|
3840
|
-
const entries = (0,
|
|
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,
|
|
3843
|
-
return entry.endsWith(".md") && (0,
|
|
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,
|
|
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,
|
|
4068
|
+
if ((0, import_fs5.existsSync)(contextsDir)) {
|
|
3883
4069
|
try {
|
|
3884
|
-
const entries = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
4097
|
+
if (!(0, import_fs5.existsSync)(domainPath)) continue;
|
|
3912
4098
|
const domainFiles = [];
|
|
3913
|
-
const ctxInstructions = (0,
|
|
3914
|
-
if ((0,
|
|
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,
|
|
3919
|
-
if ((0,
|
|
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,
|
|
3924
|
-
if ((0,
|
|
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,
|
|
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,
|
|
3932
|
-
if ((0,
|
|
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,
|
|
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
|
|
3956
|
-
|
|
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,
|
|
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,
|
|
3979
|
-
if (!(0,
|
|
3980
|
-
const filePath = (0,
|
|
3981
|
-
if (!(0,
|
|
3982
|
-
(0,
|
|
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,
|
|
3994
|
-
if (!(0,
|
|
3995
|
-
const filePath = (0,
|
|
3996
|
-
if (!(0,
|
|
3997
|
-
(0,
|
|
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
|
}
|
|
@@ -4010,6 +4217,57 @@ alwaysApply: true
|
|
|
4010
4217
|
}
|
|
4011
4218
|
}
|
|
4012
4219
|
|
|
4220
|
+
// src/utils/setup/setup-gitignore.ts
|
|
4221
|
+
var import_fs6 = require("fs");
|
|
4222
|
+
var import_path6 = require("path");
|
|
4223
|
+
var MARKER_START2 = "# one-shot-installer:start";
|
|
4224
|
+
var MARKER_END2 = "# one-shot-installer:end";
|
|
4225
|
+
var GITIGNORE_ENTRIES = [
|
|
4226
|
+
"# One-Shot installer generated files",
|
|
4227
|
+
".one-shot-state.json",
|
|
4228
|
+
".mcp.json",
|
|
4229
|
+
"_ai-context/",
|
|
4230
|
+
"_bmad/",
|
|
4231
|
+
"_bmad-output/",
|
|
4232
|
+
"",
|
|
4233
|
+
"# Claude Code local/generated files",
|
|
4234
|
+
".claude/agents/",
|
|
4235
|
+
".claude/agent-memory/",
|
|
4236
|
+
".claude/plans/",
|
|
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/"
|
|
4251
|
+
];
|
|
4252
|
+
function setupGitignore(projectPath) {
|
|
4253
|
+
const gitignorePath = (0, import_path6.join)(projectPath, ".gitignore");
|
|
4254
|
+
const block = [MARKER_START2, ...GITIGNORE_ENTRIES, MARKER_END2].join("\n");
|
|
4255
|
+
if (!(0, import_fs6.existsSync)(gitignorePath)) {
|
|
4256
|
+
(0, import_fs6.writeFileSync)(gitignorePath, block + "\n", "utf-8");
|
|
4257
|
+
return;
|
|
4258
|
+
}
|
|
4259
|
+
const existing = (0, import_fs6.readFileSync)(gitignorePath, "utf-8");
|
|
4260
|
+
const start = existing.indexOf(MARKER_START2);
|
|
4261
|
+
const end = existing.indexOf(MARKER_END2);
|
|
4262
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
4263
|
+
const updated = existing.slice(0, start) + block + existing.slice(end + MARKER_END2.length);
|
|
4264
|
+
(0, import_fs6.writeFileSync)(gitignorePath, updated, "utf-8");
|
|
4265
|
+
} else {
|
|
4266
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
4267
|
+
(0, import_fs6.writeFileSync)(gitignorePath, existing + separator + block + "\n", "utf-8");
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4013
4271
|
// src/utils/mod.ts
|
|
4014
4272
|
async function loadWizardOptions(token, repo) {
|
|
4015
4273
|
const remote = await fetchWizardOptions(token, repo);
|
|
@@ -4030,7 +4288,7 @@ async function loadWizardOptions(token, repo) {
|
|
|
4030
4288
|
}
|
|
4031
4289
|
|
|
4032
4290
|
// src/index.ts
|
|
4033
|
-
var INSTALLER_VERSION = "0.
|
|
4291
|
+
var INSTALLER_VERSION = "0.5.0";
|
|
4034
4292
|
function getInstructionFilePath(agent) {
|
|
4035
4293
|
switch (agent) {
|
|
4036
4294
|
case "claude-code":
|
|
@@ -4061,10 +4319,10 @@ function formatMCPCommand(server) {
|
|
|
4061
4319
|
}
|
|
4062
4320
|
function waitForEnter() {
|
|
4063
4321
|
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
4064
|
-
return new Promise((
|
|
4322
|
+
return new Promise((resolve4) => {
|
|
4065
4323
|
rl.question(" Press Enter to close...", () => {
|
|
4066
4324
|
rl.close();
|
|
4067
|
-
|
|
4325
|
+
resolve4();
|
|
4068
4326
|
});
|
|
4069
4327
|
});
|
|
4070
4328
|
}
|
|
@@ -4073,11 +4331,17 @@ async function main() {
|
|
|
4073
4331
|
console.log("One-Shot Setup Installer\n");
|
|
4074
4332
|
try {
|
|
4075
4333
|
const token = await getGitHubToken();
|
|
4076
|
-
console.log(" Locating
|
|
4077
|
-
const repo = await
|
|
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
|
+
}
|
|
4078
4342
|
console.log(" Loading configuration...");
|
|
4079
4343
|
const options = await loadWizardOptions(token, repo);
|
|
4080
|
-
const config = await collectInputs(options, options.releaseVersion);
|
|
4344
|
+
const config = await collectInputs(options, options.releaseVersion, !!factoryRepo);
|
|
4081
4345
|
if (!config) {
|
|
4082
4346
|
await waitForEnter();
|
|
4083
4347
|
return;
|
|
@@ -4088,7 +4352,7 @@ async function main() {
|
|
|
4088
4352
|
await waitForEnter();
|
|
4089
4353
|
return;
|
|
4090
4354
|
}
|
|
4091
|
-
const result = await execute(config, token, repo);
|
|
4355
|
+
const result = await execute(config, token, repo, factoryRepo);
|
|
4092
4356
|
printSummary(result);
|
|
4093
4357
|
return;
|
|
4094
4358
|
} catch (error) {
|
|
@@ -4096,7 +4360,7 @@ async function main() {
|
|
|
4096
4360
|
await waitForEnter();
|
|
4097
4361
|
}
|
|
4098
4362
|
}
|
|
4099
|
-
async function collectInputs(options, releaseVersion) {
|
|
4363
|
+
async function collectInputs(options, releaseVersion, factoryAvailable = false) {
|
|
4100
4364
|
const selectedIds = await esm_default2({
|
|
4101
4365
|
message: "Personas:",
|
|
4102
4366
|
instructions: " Space to select \xB7 Enter to confirm",
|
|
@@ -4128,16 +4392,17 @@ async function collectInputs(options, releaseVersion) {
|
|
|
4128
4392
|
}
|
|
4129
4393
|
const projectInput = await esm_default4({
|
|
4130
4394
|
message: "Project directory:",
|
|
4131
|
-
default: (0,
|
|
4395
|
+
default: (0, import_path7.resolve)(process.cwd())
|
|
4132
4396
|
});
|
|
4133
4397
|
return {
|
|
4134
4398
|
personas: selectedPersonas,
|
|
4135
4399
|
agent,
|
|
4136
4400
|
azureDevOpsOrg,
|
|
4137
|
-
projectPath: (0,
|
|
4401
|
+
projectPath: (0, import_path7.resolve)(projectInput),
|
|
4138
4402
|
baseMcpServers: options.baseMcpServers,
|
|
4139
4403
|
mcpConfig,
|
|
4140
|
-
releaseVersion
|
|
4404
|
+
releaseVersion,
|
|
4405
|
+
installFactory: factoryAvailable
|
|
4141
4406
|
};
|
|
4142
4407
|
}
|
|
4143
4408
|
async function previewAndConfirm(config, options) {
|
|
@@ -4151,6 +4416,7 @@ async function previewAndConfirm(config, options) {
|
|
|
4151
4416
|
console.log("\u2500".repeat(48) + "\n");
|
|
4152
4417
|
console.log(` Personas ${personaNames}`);
|
|
4153
4418
|
console.log(` Tool ${agentDisplay}`);
|
|
4419
|
+
console.log(` Factory ${config.installFactory ? "yes" : "no"}`);
|
|
4154
4420
|
console.log(` Directory ${config.projectPath}`);
|
|
4155
4421
|
console.log("");
|
|
4156
4422
|
console.log(` Instruction file`);
|
|
@@ -4170,7 +4436,7 @@ async function previewAndConfirm(config, options) {
|
|
|
4170
4436
|
console.log("\n" + "\u2500".repeat(48));
|
|
4171
4437
|
return esm_default3({ message: "Proceed?", default: true });
|
|
4172
4438
|
}
|
|
4173
|
-
async function execute(config, token, repo) {
|
|
4439
|
+
async function execute(config, token, repo, factoryRepo = null) {
|
|
4174
4440
|
const instructionFilePath = getInstructionFilePath(config.agent);
|
|
4175
4441
|
const mcpConfigPath = getMCPConfigPath(config.agent);
|
|
4176
4442
|
const result = {
|
|
@@ -4182,7 +4448,9 @@ async function execute(config, token, repo) {
|
|
|
4182
4448
|
instructionFilePath,
|
|
4183
4449
|
mcpConfigured: false,
|
|
4184
4450
|
mcpConfigPath,
|
|
4185
|
-
mcpServersAdded: []
|
|
4451
|
+
mcpServersAdded: [],
|
|
4452
|
+
gitignoreUpdated: false,
|
|
4453
|
+
factoryInstalled: false
|
|
4186
4454
|
};
|
|
4187
4455
|
console.log("");
|
|
4188
4456
|
const uniqueDomains = [...new Set(config.personas.flatMap((p) => p.domains))];
|
|
@@ -4206,6 +4474,22 @@ async function execute(config, token, repo) {
|
|
|
4206
4474
|
console.log(` ${domain.padEnd(domainColWidth)}${status}`);
|
|
4207
4475
|
}
|
|
4208
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
|
+
}
|
|
4209
4493
|
if (result.domainsInstalled.length === 0) {
|
|
4210
4494
|
result.success = false;
|
|
4211
4495
|
return result;
|
|
@@ -4221,7 +4505,7 @@ async function execute(config, token, repo) {
|
|
|
4221
4505
|
const filteredMcpConfig = Object.fromEntries(
|
|
4222
4506
|
Object.entries(config.mcpConfig).filter(([name]) => successfulMcpServers.has(name))
|
|
4223
4507
|
);
|
|
4224
|
-
const statePath = (0,
|
|
4508
|
+
const statePath = (0, import_path7.join)(config.projectPath, ".one-shot-state.json");
|
|
4225
4509
|
const allDomains = result.domainsInstalled;
|
|
4226
4510
|
process.stdout.write(` Writing ${instructionFilePath}... `);
|
|
4227
4511
|
try {
|
|
@@ -4229,7 +4513,8 @@ async function execute(config, token, repo) {
|
|
|
4229
4513
|
domains: allDomains,
|
|
4230
4514
|
agent: config.agent,
|
|
4231
4515
|
mcpConfig: filteredMcpConfig,
|
|
4232
|
-
projectPath: config.projectPath
|
|
4516
|
+
projectPath: config.projectPath,
|
|
4517
|
+
factoryInstalled: result.factoryInstalled
|
|
4233
4518
|
});
|
|
4234
4519
|
result.instructionsCreated = true;
|
|
4235
4520
|
console.log("\u2713");
|
|
@@ -4253,6 +4538,15 @@ async function execute(config, token, repo) {
|
|
|
4253
4538
|
console.log(` Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4254
4539
|
console.log(` Path: ${mcpConfigPath}`);
|
|
4255
4540
|
}
|
|
4541
|
+
process.stdout.write(` Updating .gitignore... `);
|
|
4542
|
+
try {
|
|
4543
|
+
setupGitignore(config.projectPath);
|
|
4544
|
+
result.gitignoreUpdated = true;
|
|
4545
|
+
console.log("\u2713");
|
|
4546
|
+
} catch (error) {
|
|
4547
|
+
console.log("\u2717");
|
|
4548
|
+
console.log(` Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4549
|
+
}
|
|
4256
4550
|
result.success = result.domainsFailed.length === 0 && result.instructionsCreated && result.mcpConfigured;
|
|
4257
4551
|
const allPersonas = config.personas.map((p) => p.id);
|
|
4258
4552
|
const state = {
|
|
@@ -4265,14 +4559,17 @@ async function execute(config, token, repo) {
|
|
|
4265
4559
|
domains: allDomains,
|
|
4266
4560
|
mcpServers: Object.keys(filteredMcpConfig),
|
|
4267
4561
|
mcpConfigs: filteredMcpConfig,
|
|
4562
|
+
factoryInstalled: result.factoryInstalled,
|
|
4268
4563
|
files: {
|
|
4269
4564
|
instructions: instructionFilePath,
|
|
4270
4565
|
mcpConfig: mcpConfigPath,
|
|
4271
|
-
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")
|
|
4272
4569
|
}
|
|
4273
4570
|
};
|
|
4274
4571
|
try {
|
|
4275
|
-
(0,
|
|
4572
|
+
(0, import_fs7.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
4276
4573
|
} catch {
|
|
4277
4574
|
}
|
|
4278
4575
|
return result;
|
|
@@ -4291,6 +4588,12 @@ function printSummary(result) {
|
|
|
4291
4588
|
const serverList = result.mcpServersAdded.length > 0 ? ` (${result.mcpServersAdded.join(", ")})` : "";
|
|
4292
4589
|
console.log(` ${result.mcpConfigPath} written${serverList}`);
|
|
4293
4590
|
}
|
|
4591
|
+
if (result.factoryInstalled) {
|
|
4592
|
+
console.log(` Factory installed (.claude/ + factory/)`);
|
|
4593
|
+
}
|
|
4594
|
+
if (result.gitignoreUpdated) {
|
|
4595
|
+
console.log(` .gitignore updated`);
|
|
4596
|
+
}
|
|
4294
4597
|
if (hasErrors) {
|
|
4295
4598
|
console.log("\n What went wrong:");
|
|
4296
4599
|
if (result.domainsFailed.length > 0) {
|