@altotyler/alto-rootstock-cli 1.0.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 +140 -0
- package/bin/altors.js +55 -0
- package/package.json +24 -0
- package/src/commands/install.js +151 -0
- package/src/commands/new.js +137 -0
- package/src/commands/update.js +55 -0
- package/src/lib/config.js +36 -0
- package/src/lib/fetcher.js +40 -0
- package/src/lib/scaffold.js +78 -0
- package/src/lib/updater.js +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# alto-rootstock-cli
|
|
2
|
+
|
|
3
|
+
CLI for creating and managing Rootstock Salesforce DX projects with AI agent scaffolding pre-installed for Claude Code, Cursor, and VS Code Copilot.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
altors new Create a new SFDX project with Rootstock agent skills injected
|
|
9
|
+
altors update Update Rootstock skill files in the current project
|
|
10
|
+
altors install Install/update the global VS Code Copilot agent + save GitHub token
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
After `altors new`, the generated project contains:
|
|
14
|
+
- `.claude/CLAUDE.md` + `.claude/skills/` — Claude Code (router + 10 skill files)
|
|
15
|
+
- `.cursor/rules/rootstock.mdc` — Cursor AI
|
|
16
|
+
- `.github/agents/Rootstock Agent.agent.md` — VS Code Copilot
|
|
17
|
+
- `.github/copilot-instructions.md` — VS Code Copilot always-on
|
|
18
|
+
- `.vscode/mcp.json` — Salesforce DX MCP server
|
|
19
|
+
- `.vscode/tasks.json` — Command palette tasks (see below)
|
|
20
|
+
|
|
21
|
+
## VS Code / Cursor Command Palette
|
|
22
|
+
|
|
23
|
+
Once a project is open, `Ctrl+Shift+P` → **"Tasks: Run Task"** exposes:
|
|
24
|
+
- **Rootstock: Update Skills** — pulls latest skill files from the distribution repo
|
|
25
|
+
- **Rootstock: Check Version** — shows installed CLI version
|
|
26
|
+
- **Rootstock: Install Global Agent** — installs the VS Code Copilot agent globally
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Deploying and packaging
|
|
31
|
+
|
|
32
|
+
### 1. Create the GitHub repo
|
|
33
|
+
|
|
34
|
+
Create a **private** repo: `github.com/alto-tyler/alto-rootstock-cli`
|
|
35
|
+
|
|
36
|
+
This repo is separate from `rootstock-agent-distribution` — it's the CLI source code only.
|
|
37
|
+
|
|
38
|
+
### 2. Push the code
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd alto-rootstock-cli
|
|
42
|
+
git init
|
|
43
|
+
git add .
|
|
44
|
+
git commit -m "Initial release"
|
|
45
|
+
git remote add origin git@github.com:alto-tyler/alto-rootstock-cli.git
|
|
46
|
+
git push -u origin main
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Publish a version
|
|
50
|
+
|
|
51
|
+
Tag a release — GitHub Actions publishes it automatically to GitHub Packages:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git tag v1.0.0
|
|
55
|
+
git push origin v1.0.0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The workflow in `.github/workflows/publish.yml` runs `npm publish` to npmjs.com using the `NPM_TOKEN` secret (add this in the repo Settings → Secrets → Actions).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## How users install it
|
|
63
|
+
|
|
64
|
+
No auth required — the package is public on npmjs.com:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install -g @altotyler/alto-rootstock-cli
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
After install, run `altors install` once to save your GitHub token for fetching skill files from the private distribution repo.
|
|
71
|
+
|
|
72
|
+
After the one-time setup, updates are one command:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm update -g @altotyler/alto-rootstock-cli
|
|
76
|
+
altors install # updates global VS Code agent files
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## How the update notification works
|
|
82
|
+
|
|
83
|
+
Every time `altors` runs any command, it fetches `version.json` from the distribution repo in the background (2.5s timeout, non-blocking). If the remote `cliVersion` is newer than the installed version, it prints after the command:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
┌──────────────────────────────────────────────────────┐
|
|
87
|
+
│ Update available: 1.0.0 → 1.2.0 │
|
|
88
|
+
│ Run: npm update -g @altotyler/alto-rootstock-cli │
|
|
89
|
+
└──────────────────────────────────────────────────────┘
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
To support this, keep `version.json` in `rootstock-agent-distribution` updated with a `cliVersion` field:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"version": "1.0.0",
|
|
97
|
+
"cliVersion": "1.2.0"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Repository layout
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
alto-rootstock-cli/ ← this repo (CLI source, publish to GitHub Packages)
|
|
107
|
+
├── bin/altors.js
|
|
108
|
+
├── src/
|
|
109
|
+
│ ├── commands/new.js
|
|
110
|
+
│ ├── commands/update.js
|
|
111
|
+
│ ├── commands/install.js
|
|
112
|
+
│ └── lib/
|
|
113
|
+
│ ├── config.js ← ~/.alto-rootstock/config.json + ~/.npmrc auth
|
|
114
|
+
│ ├── fetcher.js ← GitHub raw file fetcher (auth-aware)
|
|
115
|
+
│ ├── scaffold.js ← writes IDE files into project
|
|
116
|
+
│ └── updater.js ← version check (non-blocking)
|
|
117
|
+
├── project-template/ ← template files (publish to rootstock-agent-distribution)
|
|
118
|
+
│ └── vscode/tasks.json
|
|
119
|
+
└── .github/workflows/publish.yml
|
|
120
|
+
|
|
121
|
+
rootstock-agent-distribution/ ← separate repo (skill files + version manifest)
|
|
122
|
+
├── version.json ← bump cliVersion here to trigger update notices
|
|
123
|
+
└── project-template/
|
|
124
|
+
├── claude/CLAUDE.md
|
|
125
|
+
├── claude/skills/*.md
|
|
126
|
+
├── cursor/rules/rootstock.mdc
|
|
127
|
+
├── github/agents/Rootstock Agent.agent.md
|
|
128
|
+
├── github/copilot-instructions.md
|
|
129
|
+
└── vscode/mcp.json + tasks.json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Releasing a new version
|
|
135
|
+
|
|
136
|
+
1. Update `version` in `package.json`
|
|
137
|
+
2. Commit and tag: `git tag v1.x.x && git push origin v1.x.x`
|
|
138
|
+
3. GitHub Actions publishes to GitHub Packages
|
|
139
|
+
4. Update `cliVersion` in `rootstock-agent-distribution/version.json`
|
|
140
|
+
5. Users see the update prompt on their next `altors` command
|
package/bin/altors.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { checkForUpdate } = require('../src/lib/updater');
|
|
7
|
+
const pkg = require('../package.json');
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('altors')
|
|
13
|
+
.description('Rootstock Salesforce DX project creator and agent manager')
|
|
14
|
+
.version(pkg.version, '-v, --version', 'Show installed version');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('new')
|
|
18
|
+
.description('Create a new Salesforce DX project with Rootstock agent scaffolding')
|
|
19
|
+
.action(async () => {
|
|
20
|
+
const { run } = require('../src/commands/new');
|
|
21
|
+
await run();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('update')
|
|
26
|
+
.description('Update Rootstock agent skill files in the current project')
|
|
27
|
+
.action(async () => {
|
|
28
|
+
const { run } = require('../src/commands/update');
|
|
29
|
+
await run();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('install')
|
|
34
|
+
.description('Install or update the global Rootstock agent for VS Code Copilot')
|
|
35
|
+
.action(async () => {
|
|
36
|
+
const { run } = require('../src/commands/install');
|
|
37
|
+
await run();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
(async () => {
|
|
41
|
+
// Non-blocking version check — runs in background, prints after command
|
|
42
|
+
const updatePromise = checkForUpdate(pkg.version).catch(() => null);
|
|
43
|
+
|
|
44
|
+
await program.parseAsync(process.argv);
|
|
45
|
+
|
|
46
|
+
// Print update notice after command output if one is available
|
|
47
|
+
const update = await updatePromise;
|
|
48
|
+
if (update) {
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(chalk.yellow('┌─────────────────────────────────────────────────────┐'));
|
|
51
|
+
console.log(chalk.yellow('│') + chalk.white(` Update available: ${chalk.dim(update.current)} → ${chalk.green(update.latest)}`.padEnd(53)) + chalk.yellow('│'));
|
|
52
|
+
console.log(chalk.yellow('│') + chalk.white(` Run: ${chalk.cyan('npm update -g @altotyler/alto-rootstock-cli')}`.padEnd(53)) + chalk.yellow('│'));
|
|
53
|
+
console.log(chalk.yellow('└─────────────────────────────────────────────────────┘'));
|
|
54
|
+
}
|
|
55
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@altotyler/alto-rootstock-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for creating and managing Rootstock Salesforce DX projects with AI agent scaffolding",
|
|
5
|
+
"bin": {
|
|
6
|
+
"altors": "./bin/altors.js"
|
|
7
|
+
},
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18.0.0"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"registry": "https://registry.npmjs.org",
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/alto-tyler/alto-rootstock-cli.git"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"chalk": "^4.1.2",
|
|
21
|
+
"commander": "^12.1.0",
|
|
22
|
+
"prompts": "^2.4.2"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const prompts = require('prompts');
|
|
8
|
+
const { fetchRemoteFile } = require('../lib/fetcher');
|
|
9
|
+
const { getToken, saveToken } = require('../lib/config');
|
|
10
|
+
|
|
11
|
+
// VS Code Copilot global agent directory by platform
|
|
12
|
+
function getVsCodeAgentDir() {
|
|
13
|
+
switch (process.platform) {
|
|
14
|
+
case 'win32':
|
|
15
|
+
return path.join(process.env.APPDATA || '', 'Code', 'User', 'prompts', 'agents');
|
|
16
|
+
case 'darwin':
|
|
17
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'prompts', 'agents');
|
|
18
|
+
default:
|
|
19
|
+
return path.join(os.homedir(), '.config', 'Code', 'User', 'prompts', 'agents');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Global Claude Code commands directory
|
|
24
|
+
function getClaudeCommandsDir() {
|
|
25
|
+
return path.join(os.homedir(), '.claude', 'commands');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const GLOBAL_FILES = [
|
|
29
|
+
{
|
|
30
|
+
remote: 'project-template/github/agents/Rootstock Agent.agent.md',
|
|
31
|
+
getTarget: () => path.join(getVsCodeAgentDir(), 'Rootstock Agent.agent.md'),
|
|
32
|
+
label: 'VS Code Copilot global agent',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
async function promptForToken() {
|
|
37
|
+
console.log();
|
|
38
|
+
console.log(chalk.yellow(' No GitHub token found.'));
|
|
39
|
+
console.log(chalk.dim(' A token with read:packages scope is required to access the private distribution repo.'));
|
|
40
|
+
console.log();
|
|
41
|
+
|
|
42
|
+
const { token } = await prompts(
|
|
43
|
+
{
|
|
44
|
+
type: 'password',
|
|
45
|
+
name: 'token',
|
|
46
|
+
message: 'GitHub Personal Access Token (read:packages)',
|
|
47
|
+
},
|
|
48
|
+
{ onCancel: () => process.exit(0) }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!token) return null;
|
|
52
|
+
|
|
53
|
+
const { save } = await prompts(
|
|
54
|
+
{
|
|
55
|
+
type: 'confirm',
|
|
56
|
+
name: 'save',
|
|
57
|
+
message: 'Save token to ~/.alto-rootstock/config.json for future use?',
|
|
58
|
+
initial: true,
|
|
59
|
+
},
|
|
60
|
+
{ onCancel: () => {} }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (save) {
|
|
64
|
+
saveToken(token);
|
|
65
|
+
console.log(` ${chalk.green('✓')} Token saved to ~/.alto-rootstock/config.json`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Also write to ~/.npmrc so npm update works without re-entering the token
|
|
69
|
+
const npmrcPath = path.join(os.homedir(), '.npmrc');
|
|
70
|
+
const npmrcLine = `//npm.pkg.github.com/:_authToken=${token}`;
|
|
71
|
+
const npmrcScope = '@alto-tyler:registry=https://npm.pkg.github.com';
|
|
72
|
+
|
|
73
|
+
let npmrcContent = '';
|
|
74
|
+
if (fs.existsSync(npmrcPath)) {
|
|
75
|
+
npmrcContent = fs.readFileSync(npmrcPath, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let updated = false;
|
|
79
|
+
if (!npmrcContent.includes('//npm.pkg.github.com/:_authToken=')) {
|
|
80
|
+
npmrcContent += `\n${npmrcLine}\n`;
|
|
81
|
+
updated = true;
|
|
82
|
+
} else {
|
|
83
|
+
// Replace existing token line
|
|
84
|
+
npmrcContent = npmrcContent.replace(/\/\/npm\.pkg\.github\.com\/:_authToken=.*/g, npmrcLine);
|
|
85
|
+
updated = true;
|
|
86
|
+
}
|
|
87
|
+
if (!npmrcContent.includes('@alto-tyler:registry=')) {
|
|
88
|
+
npmrcContent += `${npmrcScope}\n`;
|
|
89
|
+
updated = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (updated) {
|
|
93
|
+
fs.writeFileSync(npmrcPath, npmrcContent.trimStart(), 'utf8');
|
|
94
|
+
console.log(` ${chalk.green('✓')} ~/.npmrc updated — ${chalk.cyan('npm update -g @altotyler/alto-rootstock-cli')} will work without re-entering a token`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
process.env.GITHUB_TOKEN = token;
|
|
98
|
+
return token;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function run() {
|
|
102
|
+
console.log();
|
|
103
|
+
console.log(chalk.bold(' Rootstock: Global Install / Update'));
|
|
104
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────────'));
|
|
105
|
+
console.log();
|
|
106
|
+
|
|
107
|
+
let token = getToken();
|
|
108
|
+
if (!token) {
|
|
109
|
+
token = await promptForToken();
|
|
110
|
+
if (!token) {
|
|
111
|
+
console.error(chalk.red(' ✗ No token provided. Cannot access private distribution repo.'));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.log(` ${chalk.green('✓')} GitHub token found`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log();
|
|
119
|
+
|
|
120
|
+
for (const entry of GLOBAL_FILES) {
|
|
121
|
+
const target = entry.getTarget();
|
|
122
|
+
const dir = path.dirname(target);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
process.stdout.write(chalk.dim(` Fetching ${entry.label}...`));
|
|
126
|
+
const content = await fetchRemoteFile(entry.remote);
|
|
127
|
+
|
|
128
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
129
|
+
const existed = fs.existsSync(target);
|
|
130
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
131
|
+
|
|
132
|
+
const label = existed ? chalk.blue('↑ Updated') : chalk.green('+ Installed');
|
|
133
|
+
process.stdout.write(`\r ${label}: ${target}\n`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
process.stdout.write(`\r ${chalk.red('✗')} ${entry.label}: ${err.message}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log();
|
|
140
|
+
console.log(chalk.bold(' After install:'));
|
|
141
|
+
console.log(chalk.dim(' 1. Reload VS Code (or open a new chat window)'));
|
|
142
|
+
console.log(chalk.dim(' 2. Select "Rootstock Agent" in the chat agent picker'));
|
|
143
|
+
console.log(chalk.dim(' 3. For project-level skills: run altors new or altors update in your project'));
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(chalk.bold(' To update in future:'));
|
|
146
|
+
console.log(chalk.cyan(' npm update -g @altotyler/alto-rootstock-cli'));
|
|
147
|
+
console.log(chalk.cyan(' altors install'));
|
|
148
|
+
console.log();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { run };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const prompts = require('prompts');
|
|
8
|
+
const { injectScaffolding } = require('../lib/scaffold');
|
|
9
|
+
const { getToken } = require('../lib/config');
|
|
10
|
+
|
|
11
|
+
function banner() {
|
|
12
|
+
console.log();
|
|
13
|
+
console.log(chalk.bold.blue(' ┌─────────────────────────────────────────────┐'));
|
|
14
|
+
console.log(chalk.bold.blue(' │') + chalk.bold.white(' alto-rootstock-cli ') + chalk.bold.blue('│'));
|
|
15
|
+
console.log(chalk.bold.blue(' │') + chalk.dim(' Rootstock Salesforce Project Creator ') + chalk.bold.blue('│'));
|
|
16
|
+
console.log(chalk.bold.blue(' └─────────────────────────────────────────────┘'));
|
|
17
|
+
console.log();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function checkSfCli() {
|
|
21
|
+
const result = spawnSync('sf', ['--version'], { encoding: 'utf8', shell: true });
|
|
22
|
+
if (result.status !== 0 || result.error) {
|
|
23
|
+
console.error(chalk.red(' ✗ Salesforce CLI (sf) not found.'));
|
|
24
|
+
console.error(chalk.dim(' Install it from: https://developer.salesforce.com/tools/salesforcecli'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const version = (result.stdout || '').split('\n')[0].trim();
|
|
28
|
+
console.log(` ${chalk.green('✓')} Salesforce CLI detected ${chalk.dim(`(${version})`)}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkGithubToken() {
|
|
32
|
+
const token = getToken();
|
|
33
|
+
if (!token) {
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(chalk.yellow(' ⚠ No GitHub token found.'));
|
|
36
|
+
console.log(chalk.dim(' Skill files are hosted on a private repo.'));
|
|
37
|
+
console.log(chalk.dim(' Set GITHUB_TOKEN or run:') + chalk.cyan(' altors install --save-token'));
|
|
38
|
+
console.log();
|
|
39
|
+
}
|
|
40
|
+
return token;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function promptProjectDetails() {
|
|
44
|
+
console.log();
|
|
45
|
+
const response = await prompts(
|
|
46
|
+
[
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
name: 'projectName',
|
|
50
|
+
message: 'Project name',
|
|
51
|
+
validate: v => v.trim().length > 0 ? true : 'Project name is required',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
name: 'outputDir',
|
|
56
|
+
message: 'Output directory',
|
|
57
|
+
initial: '.',
|
|
58
|
+
format: v => v.trim() || '.',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
{
|
|
62
|
+
onCancel: () => {
|
|
63
|
+
console.log(chalk.dim('\n Cancelled.'));
|
|
64
|
+
process.exit(0);
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runSfProjectGenerate(projectName, outputDir) {
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(chalk.dim(` Running: sf project generate --manifest --name ${projectName} --output-dir ${outputDir}`));
|
|
74
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────────'));
|
|
75
|
+
console.log();
|
|
76
|
+
|
|
77
|
+
// stdio: inherit lets sf prompt the user for remaining questions
|
|
78
|
+
// (default package dir, API version, etc.) natively
|
|
79
|
+
const result = spawnSync(
|
|
80
|
+
'sf',
|
|
81
|
+
['project', 'generate', '--manifest', '--name', projectName, '--output-dir', outputDir],
|
|
82
|
+
{ stdio: 'inherit', shell: true }
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
console.log();
|
|
86
|
+
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
console.error(chalk.red(' ✗ sf project generate failed.'));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function run() {
|
|
94
|
+
banner();
|
|
95
|
+
checkSfCli();
|
|
96
|
+
checkGithubToken();
|
|
97
|
+
|
|
98
|
+
const { projectName, outputDir } = await promptProjectDetails();
|
|
99
|
+
|
|
100
|
+
runSfProjectGenerate(projectName, outputDir);
|
|
101
|
+
|
|
102
|
+
// sf creates the project at <outputDir>/<projectName>
|
|
103
|
+
const projectRoot = path.resolve(outputDir, projectName);
|
|
104
|
+
|
|
105
|
+
if (!fs.existsSync(projectRoot)) {
|
|
106
|
+
console.error(chalk.red(` ✗ Expected project directory not found: ${projectRoot}`));
|
|
107
|
+
console.error(chalk.dim(' sf may have used a different path. Run altors update from inside the project directory.'));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(` ${chalk.green('✓')} Salesforce DX project created`);
|
|
112
|
+
console.log();
|
|
113
|
+
console.log(chalk.bold(' Injecting Rootstock agent scaffolding...'));
|
|
114
|
+
console.log();
|
|
115
|
+
|
|
116
|
+
const results = await injectScaffolding(projectRoot);
|
|
117
|
+
|
|
118
|
+
console.log();
|
|
119
|
+
|
|
120
|
+
if (results.failed.length > 0) {
|
|
121
|
+
console.log(chalk.yellow(` ⚠ ${results.failed.length} file(s) failed to fetch. Run ${chalk.cyan('altors update')} inside the project to retry.`));
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(chalk.bold.blue(' ┌─────────────────────────────────────────────┐'));
|
|
126
|
+
console.log(chalk.bold.blue(' │') + chalk.bold.green(` ✓ Project ready: ${path.relative(process.cwd(), projectRoot)}`.padEnd(46)) + chalk.bold.blue('│'));
|
|
127
|
+
console.log(chalk.bold.blue(' └─────────────────────────────────────────────┘'));
|
|
128
|
+
console.log();
|
|
129
|
+
console.log(chalk.bold(' Next steps:'));
|
|
130
|
+
console.log(chalk.dim(` cd ${path.relative(process.cwd(), projectRoot)}`));
|
|
131
|
+
console.log(chalk.dim(' sf org login web --alias myorg'));
|
|
132
|
+
console.log(chalk.dim(' code . # open in VS Code'));
|
|
133
|
+
console.log(chalk.dim(' # Reload VS Code to activate the Salesforce DX MCP server'));
|
|
134
|
+
console.log();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { run };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { updateScaffolding } = require('../lib/scaffold');
|
|
7
|
+
|
|
8
|
+
function findProjectRoot(startDir) {
|
|
9
|
+
let dir = startDir;
|
|
10
|
+
for (let i = 0; i < 10; i++) {
|
|
11
|
+
if (fs.existsSync(path.join(dir, 'sfdx-project.json'))) return dir;
|
|
12
|
+
const parent = path.dirname(dir);
|
|
13
|
+
if (parent === dir) break;
|
|
14
|
+
dir = parent;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function run() {
|
|
20
|
+
console.log();
|
|
21
|
+
console.log(chalk.bold(' Rootstock: Update Skills'));
|
|
22
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────────'));
|
|
23
|
+
console.log();
|
|
24
|
+
|
|
25
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
26
|
+
|
|
27
|
+
if (!projectRoot) {
|
|
28
|
+
console.error(chalk.red(' ✗ No Salesforce DX project found (sfdx-project.json missing).'));
|
|
29
|
+
console.error(chalk.dim(' Run this command from inside a Salesforce DX project directory.'));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(` ${chalk.dim('Project root:')} ${projectRoot}`);
|
|
34
|
+
console.log();
|
|
35
|
+
|
|
36
|
+
const results = await updateScaffolding(projectRoot);
|
|
37
|
+
|
|
38
|
+
const total = results.updated.length + results.added.length;
|
|
39
|
+
console.log();
|
|
40
|
+
|
|
41
|
+
if (results.failed.length > 0) {
|
|
42
|
+
console.log(chalk.yellow(` ⚠ ${results.failed.length} file(s) could not be fetched.`));
|
|
43
|
+
console.log(chalk.dim(' Check your GITHUB_TOKEN or network connection.'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (total === 0 && results.failed.length === 0) {
|
|
47
|
+
console.log(chalk.green(' ✓ All skill files are already up to date.'));
|
|
48
|
+
} else {
|
|
49
|
+
console.log(chalk.green(` ✓ Done.`) + chalk.dim(` ${results.updated.length} updated, ${results.added.length} added, ${results.failed.length} failed.`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { run };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.alto-rootstock');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
baseUrl: 'https://raw.githubusercontent.com/alto-tyler/rootstock-agent-distribution/main',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function load() {
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
17
|
+
return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) };
|
|
18
|
+
}
|
|
19
|
+
} catch (_) {}
|
|
20
|
+
return { ...DEFAULTS };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function save(data) {
|
|
24
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
25
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getToken() {
|
|
29
|
+
return process.env.GITHUB_TOKEN || load().token || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveToken(token) {
|
|
33
|
+
save({ ...load(), token });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { load, save, getToken, saveToken, CONFIG_FILE };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const { load, getToken } = require('./config');
|
|
5
|
+
|
|
6
|
+
function fetchUrl(url, token) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const opts = new URL(url);
|
|
9
|
+
const headers = { 'User-Agent': 'alto-rootstock-cli' };
|
|
10
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
11
|
+
|
|
12
|
+
https.get({ hostname: opts.hostname, path: opts.pathname + opts.search, headers }, (res) => {
|
|
13
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
14
|
+
return fetchUrl(res.headers.location, token).then(resolve).catch(reject);
|
|
15
|
+
}
|
|
16
|
+
if (res.statusCode !== 200) {
|
|
17
|
+
res.resume();
|
|
18
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
19
|
+
}
|
|
20
|
+
const chunks = [];
|
|
21
|
+
res.on('data', c => chunks.push(c));
|
|
22
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
23
|
+
res.on('error', reject);
|
|
24
|
+
}).on('error', reject);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function fetchRemoteFile(remotePath) {
|
|
29
|
+
const { baseUrl } = load();
|
|
30
|
+
const token = getToken();
|
|
31
|
+
const url = `${baseUrl}/${remotePath}`;
|
|
32
|
+
return fetchUrl(url, token);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchRemoteJson(remotePath) {
|
|
36
|
+
const text = await fetchRemoteFile(remotePath);
|
|
37
|
+
return JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { fetchRemoteFile, fetchRemoteJson, fetchUrl };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { fetchRemoteFile } = require('./fetcher');
|
|
7
|
+
|
|
8
|
+
// Maps remote template paths → local project paths
|
|
9
|
+
const SCAFFOLD_MANIFEST = [
|
|
10
|
+
{ remote: 'project-template/claude/CLAUDE.md', local: '.claude/CLAUDE.md' },
|
|
11
|
+
{ remote: 'project-template/claude/skills/rootstock-core.md', local: '.claude/skills/rootstock-core.md' },
|
|
12
|
+
{ remote: 'project-template/claude/skills/rootstock-soapi.md', local: '.claude/skills/rootstock-soapi.md' },
|
|
13
|
+
{ remote: 'project-template/claude/skills/rootstock-sydata.md', local: '.claude/skills/rootstock-sydata.md' },
|
|
14
|
+
{ remote: 'project-template/claude/skills/rootstock-sydatat.md', local: '.claude/skills/rootstock-sydatat.md' },
|
|
15
|
+
{ remote: 'project-template/claude/skills/rootstock-poloader.md', local: '.claude/skills/rootstock-poloader.md' },
|
|
16
|
+
{ remote: 'project-template/claude/skills/rootstock-manufacturing.md', local: '.claude/skills/rootstock-manufacturing.md' },
|
|
17
|
+
{ remote: 'project-template/claude/skills/rootstock-inventory.md', local: '.claude/skills/rootstock-inventory.md' },
|
|
18
|
+
{ remote: 'project-template/claude/skills/rootstock-testing.md', local: '.claude/skills/rootstock-testing.md' },
|
|
19
|
+
{ remote: 'project-template/claude/skills/rootstock-debug.md', local: '.claude/skills/rootstock-debug.md' },
|
|
20
|
+
{ remote: 'project-template/claude/skills/rootstock-session.md', local: '.claude/skills/rootstock-session.md' },
|
|
21
|
+
{ remote: 'project-template/cursor/rules/rootstock.mdc', local: '.cursor/rules/rootstock.mdc' },
|
|
22
|
+
{ remote: 'project-template/github/agents/Rootstock Agent.agent.md', local: '.github/agents/Rootstock Agent.agent.md' },
|
|
23
|
+
{ remote: 'project-template/github/copilot-instructions.md', local: '.github/copilot-instructions.md' },
|
|
24
|
+
{ remote: 'project-template/vscode/mcp.json', local: '.vscode/mcp.json' },
|
|
25
|
+
{ remote: 'project-template/vscode/tasks.json', local: '.vscode/tasks.json' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function writeFile(projectRoot, relPath, content) {
|
|
29
|
+
const full = path.join(projectRoot, relPath);
|
|
30
|
+
const dir = path.dirname(full);
|
|
31
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
fs.writeFileSync(full, content, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function injectScaffolding(projectRoot) {
|
|
36
|
+
const results = { ok: [], failed: [] };
|
|
37
|
+
|
|
38
|
+
for (const entry of SCAFFOLD_MANIFEST) {
|
|
39
|
+
try {
|
|
40
|
+
process.stdout.write(chalk.dim(` Fetching ${entry.local}...`));
|
|
41
|
+
const content = await fetchRemoteFile(entry.remote);
|
|
42
|
+
writeFile(projectRoot, entry.local, content);
|
|
43
|
+
process.stdout.write(`\r ${chalk.green('✓')} ${entry.local}\n`);
|
|
44
|
+
results.ok.push(entry.local);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
process.stdout.write(`\r ${chalk.red('✗')} ${entry.local} ${chalk.dim(`(${err.message})`)}\n`);
|
|
47
|
+
results.failed.push(entry.local);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function updateScaffolding(projectRoot) {
|
|
55
|
+
// Same as inject but reports updated vs new
|
|
56
|
+
const results = { updated: [], added: [], failed: [] };
|
|
57
|
+
|
|
58
|
+
for (const entry of SCAFFOLD_MANIFEST) {
|
|
59
|
+
const fullPath = path.join(projectRoot, entry.local);
|
|
60
|
+
const exists = fs.existsSync(fullPath);
|
|
61
|
+
try {
|
|
62
|
+
process.stdout.write(chalk.dim(` Fetching ${entry.local}...`));
|
|
63
|
+
const content = await fetchRemoteFile(entry.remote);
|
|
64
|
+
writeFile(projectRoot, entry.local, content);
|
|
65
|
+
const label = exists ? chalk.blue('↑') : chalk.green('+');
|
|
66
|
+
process.stdout.write(`\r ${label} ${entry.local}\n`);
|
|
67
|
+
if (exists) results.updated.push(entry.local);
|
|
68
|
+
else results.added.push(entry.local);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
process.stdout.write(`\r ${chalk.red('✗')} ${entry.local} ${chalk.dim(`(${err.message})`)}\n`);
|
|
71
|
+
results.failed.push(entry.local);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { injectScaffolding, updateScaffolding, SCAFFOLD_MANIFEST };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { fetchRemoteJson } = require('./fetcher');
|
|
4
|
+
|
|
5
|
+
function parseVersion(v) {
|
|
6
|
+
return (v || '0.0.0').replace(/^v/, '').split('.').map(Number);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isNewer(latest, current) {
|
|
10
|
+
const l = parseVersion(latest);
|
|
11
|
+
const c = parseVersion(current);
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
if (l[i] > c[i]) return true;
|
|
14
|
+
if (l[i] < c[i]) return false;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function checkForUpdate(currentVersion) {
|
|
20
|
+
try {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeout = setTimeout(() => controller.abort(), 2500);
|
|
23
|
+
|
|
24
|
+
const data = await Promise.race([
|
|
25
|
+
fetchRemoteJson('version.json'),
|
|
26
|
+
new Promise((_, reject) =>
|
|
27
|
+
setTimeout(() => reject(new Error('timeout')), 2500)
|
|
28
|
+
),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
|
|
33
|
+
const latest = data.cliVersion || data.version;
|
|
34
|
+
if (latest && isNewer(latest, currentVersion)) {
|
|
35
|
+
return { current: currentVersion, latest };
|
|
36
|
+
}
|
|
37
|
+
} catch (_) {
|
|
38
|
+
// Network unavailable or timeout — silently skip
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { checkForUpdate, isNewer };
|