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.
- package/README.md +27 -84
- package/bin/installer.js +410 -104
- 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,210 @@ 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)(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,
|
|
3936
|
+
const mcpJsonPath = (0, import_path4.join)(projectPath, target.filePath);
|
|
3735
3937
|
if (target.dir) {
|
|
3736
|
-
const dir = (0,
|
|
3737
|
-
if (!(0,
|
|
3738
|
-
(0,
|
|
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,
|
|
3945
|
+
if ((0, import_fs4.existsSync)(mcpJsonPath)) {
|
|
3744
3946
|
try {
|
|
3745
|
-
const content = (0,
|
|
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,
|
|
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
|
|
3795
|
-
var
|
|
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,
|
|
3804
|
-
(0,
|
|
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,
|
|
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,
|
|
4034
|
+
(0, import_fs5.writeFileSync)(filePath, updated, "utf-8");
|
|
3833
4035
|
} else {
|
|
3834
4036
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
3835
|
-
(0,
|
|
4037
|
+
(0, import_fs5.writeFileSync)(filePath, existing + separator + marked, "utf-8");
|
|
3836
4038
|
}
|
|
3837
4039
|
}
|
|
3838
4040
|
function collectMdFiles(dir) {
|
|
3839
|
-
if (!(0,
|
|
3840
|
-
const entries = (0,
|
|
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,
|
|
3843
|
-
return entry.endsWith(".md") && (0,
|
|
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,
|
|
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,
|
|
4084
|
+
if ((0, import_fs5.existsSync)(contextsDir)) {
|
|
3883
4085
|
try {
|
|
3884
|
-
const entries = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
4113
|
+
if (!(0, import_fs5.existsSync)(domainPath)) continue;
|
|
3912
4114
|
const domainFiles = [];
|
|
3913
|
-
const ctxInstructions = (0,
|
|
3914
|
-
if ((0,
|
|
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,
|
|
3919
|
-
if ((0,
|
|
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,
|
|
3924
|
-
if ((0,
|
|
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,
|
|
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,
|
|
3932
|
-
if ((0,
|
|
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,
|
|
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
|
|
3956
|
-
|
|
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,
|
|
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,
|
|
3979
|
-
if (!(0,
|
|
3980
|
-
const filePath = (0,
|
|
3981
|
-
if (!(0,
|
|
3982
|
-
(0,
|
|
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,
|
|
3994
|
-
if (!(0,
|
|
3995
|
-
const filePath = (0,
|
|
3996
|
-
if (!(0,
|
|
3997
|
-
(0,
|
|
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
|
|
4015
|
-
var
|
|
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,
|
|
4269
|
+
const gitignorePath = (0, import_path6.join)(projectPath, ".gitignore");
|
|
4034
4270
|
const block = [MARKER_START2, ...GITIGNORE_ENTRIES, MARKER_END2].join("\n");
|
|
4035
|
-
if (!(0,
|
|
4036
|
-
(0,
|
|
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,
|
|
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,
|
|
4280
|
+
(0, import_fs6.writeFileSync)(gitignorePath, updated, "utf-8");
|
|
4045
4281
|
} else {
|
|
4046
4282
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
4047
|
-
(0,
|
|
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.
|
|
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((
|
|
4357
|
+
return new Promise((resolve4) => {
|
|
4103
4358
|
rl.question(" Press Enter to close...", () => {
|
|
4104
4359
|
rl.close();
|
|
4105
|
-
|
|
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
|
|
4115
|
-
const repo = await
|
|
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,
|
|
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,
|
|
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(`
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
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("
|
|
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,
|
|
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,
|
|
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
|
}
|