@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 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 };