@meltstudio/meltctl 2.4.1 → 4.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.
@@ -0,0 +1,5 @@
1
+ interface InitOptions {
2
+ force?: boolean;
3
+ }
4
+ export declare function initCommand(options: InitOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,149 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { authenticatedFetch, isAuthenticated } from '../utils/auth.js';
5
+ const SKILL_FRONTMATTER = {
6
+ plan: `---
7
+ user-invocable: true
8
+ description: Design an implementation approach before writing code
9
+ ---
10
+
11
+ `,
12
+ review: `---
13
+ user-invocable: true
14
+ description: Review changes against project standards
15
+ ---
16
+
17
+ `,
18
+ pr: `---
19
+ user-invocable: true
20
+ description: Create a well-structured pull request
21
+ ---
22
+
23
+ `,
24
+ debug: `---
25
+ user-invocable: true
26
+ description: Systematically investigate and fix bugs
27
+ ---
28
+
29
+ `,
30
+ };
31
+ const GITIGNORE_ENTRIES = ['.env.local', '.claude/settings.local.json'];
32
+ async function fetchTemplates() {
33
+ const response = await authenticatedFetch('/templates');
34
+ if (!response.ok) {
35
+ throw new Error(`Failed to fetch templates: ${response.statusText}`);
36
+ }
37
+ const data = (await response.json());
38
+ return data.files;
39
+ }
40
+ export async function initCommand(options) {
41
+ const cwd = process.cwd();
42
+ // Check if already initialized (AGENTS.md exists)
43
+ if (!options.force && (await fs.pathExists(path.join(cwd, 'AGENTS.md')))) {
44
+ console.log(chalk.yellow('Project already has an AGENTS.md file. Use --force to overwrite.'));
45
+ process.exit(1);
46
+ }
47
+ // Require authentication
48
+ if (!(await isAuthenticated())) {
49
+ console.error(chalk.red('Not authenticated. Run `meltctl login` first.'));
50
+ process.exit(1);
51
+ }
52
+ let templates;
53
+ try {
54
+ templates = await fetchTemplates();
55
+ }
56
+ catch (error) {
57
+ if (error instanceof Error && error.message.includes('expired')) {
58
+ console.error(chalk.red('Session expired. Run `meltctl login` to re-authenticate.'));
59
+ }
60
+ else if (error instanceof Error && error.message.includes('fetch')) {
61
+ console.error(chalk.red('Could not reach Melt API. Check your connection.'));
62
+ }
63
+ else {
64
+ console.error(chalk.red(`Failed to fetch templates: ${error instanceof Error ? error.message : 'Unknown error'}`));
65
+ }
66
+ process.exit(1);
67
+ }
68
+ console.log(chalk.bold('Initializing Melt development tools...'));
69
+ console.log();
70
+ // Copy AGENTS.md
71
+ const agentsMd = templates['agents-md.md'];
72
+ if (agentsMd) {
73
+ await fs.writeFile(path.join(cwd, 'AGENTS.md'), agentsMd, 'utf-8');
74
+ }
75
+ // Copy .claude/settings.json
76
+ const claudeSettings = templates['claude-settings.json'];
77
+ if (claudeSettings) {
78
+ await fs.ensureDir(path.join(cwd, '.claude'));
79
+ await fs.writeFile(path.join(cwd, '.claude/settings.json'), claudeSettings, 'utf-8');
80
+ }
81
+ // Create Claude skills from workflow templates
82
+ const workflows = ['plan', 'review', 'pr', 'debug'];
83
+ for (const name of workflows) {
84
+ const workflowContent = templates[`workflows/${name}.md`];
85
+ if (workflowContent) {
86
+ const skillDir = path.join(cwd, `.claude/skills/melt-${name}`);
87
+ await fs.ensureDir(skillDir);
88
+ const skillContent = SKILL_FRONTMATTER[name] + workflowContent;
89
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent, 'utf-8');
90
+ }
91
+ }
92
+ // Copy .cursor/rules/standards.mdc
93
+ const cursorRules = templates['cursor-rules.mdc'];
94
+ if (cursorRules) {
95
+ await fs.ensureDir(path.join(cwd, '.cursor/rules'));
96
+ await fs.writeFile(path.join(cwd, '.cursor/rules/standards.mdc'), cursorRules, 'utf-8');
97
+ }
98
+ // Copy Cursor commands from workflow templates
99
+ await fs.ensureDir(path.join(cwd, '.cursor/commands'));
100
+ for (const name of workflows) {
101
+ const workflowContent = templates[`workflows/${name}.md`];
102
+ if (workflowContent) {
103
+ await fs.writeFile(path.join(cwd, `.cursor/commands/melt-${name}.md`), workflowContent, 'utf-8');
104
+ }
105
+ }
106
+ // Copy .mcp.json
107
+ const mcpConfig = templates['mcp-configs/base.json'];
108
+ if (mcpConfig) {
109
+ await fs.writeFile(path.join(cwd, '.mcp.json'), mcpConfig, 'utf-8');
110
+ }
111
+ // Copy .env.melt.example
112
+ const envExample = templates['env-melt-example'];
113
+ if (envExample) {
114
+ await fs.writeFile(path.join(cwd, '.env.melt.example'), envExample, 'utf-8');
115
+ }
116
+ // Update .gitignore
117
+ await updateGitignore(cwd);
118
+ // Print summary
119
+ console.log(chalk.green('Created files:'));
120
+ console.log(chalk.dim(' AGENTS.md'));
121
+ console.log(chalk.dim(' .claude/settings.json'));
122
+ console.log(chalk.dim(' .claude/skills/melt-{plan,review,pr,debug}/SKILL.md'));
123
+ console.log(chalk.dim(' .cursor/rules/standards.mdc'));
124
+ console.log(chalk.dim(' .cursor/commands/melt-{plan,review,pr,debug}.md'));
125
+ console.log(chalk.dim(' .mcp.json'));
126
+ console.log(chalk.dim(' .env.melt.example'));
127
+ console.log();
128
+ console.log(chalk.yellow('Next steps:'));
129
+ console.log(chalk.dim(' 1. Edit AGENTS.md to describe your project and add team-specific standards'));
130
+ console.log(chalk.dim(' 2. Copy .env.melt.example to .env.local and fill in credentials'));
131
+ console.log(chalk.dim(' 3. Commit the generated files'));
132
+ console.log();
133
+ console.log(chalk.green('Done!'));
134
+ }
135
+ async function updateGitignore(cwd) {
136
+ const gitignorePath = path.join(cwd, '.gitignore');
137
+ let content = '';
138
+ if (await fs.pathExists(gitignorePath)) {
139
+ content = await fs.readFile(gitignorePath, 'utf-8');
140
+ }
141
+ const missingEntries = GITIGNORE_ENTRIES.filter(entry => !content.includes(entry));
142
+ if (missingEntries.length > 0) {
143
+ const suffix = content.endsWith('\n') || content === '' ? '' : '\n';
144
+ const section = missingEntries.length > 0
145
+ ? `${suffix}\n# Melt - local settings\n${missingEntries.join('\n')}\n`
146
+ : '';
147
+ await fs.writeFile(gitignorePath, content + section, 'utf-8');
148
+ }
149
+ }
@@ -0,0 +1 @@
1
+ export declare function loginCommand(): Promise<void>;
@@ -0,0 +1,90 @@
1
+ import chalk from 'chalk';
2
+ import http from 'http';
3
+ import { URL } from 'url';
4
+ import { exec } from 'child_process';
5
+ import { API_BASE, storeAuth } from '../utils/auth.js';
6
+ import { printBanner } from '../utils/banner.js';
7
+ const LOGIN_TIMEOUT_MS = 60_000;
8
+ function openBrowser(url) {
9
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
10
+ exec(`${command} "${url}"`);
11
+ }
12
+ function findFreePort() {
13
+ return new Promise((resolve, reject) => {
14
+ const server = http.createServer();
15
+ server.listen(0, () => {
16
+ const address = server.address();
17
+ if (address && typeof address === 'object') {
18
+ const port = address.port;
19
+ server.close(() => resolve(port));
20
+ }
21
+ else {
22
+ reject(new Error('Could not find free port'));
23
+ }
24
+ });
25
+ });
26
+ }
27
+ export async function loginCommand() {
28
+ printBanner();
29
+ console.log(chalk.bold(' Logging in to Melt...'));
30
+ const port = await findFreePort();
31
+ const redirectUri = `http://localhost:${port.toString()}`;
32
+ const authCode = await new Promise((resolve, reject) => {
33
+ const timeout = setTimeout(() => {
34
+ server.close();
35
+ reject(new Error('Authentication timed out. Please try again.'));
36
+ }, LOGIN_TIMEOUT_MS);
37
+ const server = http.createServer((req, res) => {
38
+ const url = new URL(req.url ?? '/', `http://localhost:${port.toString()}`);
39
+ const code = url.searchParams.get('code');
40
+ const error = url.searchParams.get('error');
41
+ if (error) {
42
+ res.writeHead(200, { 'Content-Type': 'text/html' });
43
+ res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab.</p></body></html>');
44
+ clearTimeout(timeout);
45
+ server.close();
46
+ reject(new Error(`Authentication denied: ${error}`));
47
+ return;
48
+ }
49
+ if (!code) {
50
+ res.writeHead(400, { 'Content-Type': 'text/html' });
51
+ res.end('<html><body><h2>Missing authorization code</h2></body></html>');
52
+ return;
53
+ }
54
+ res.writeHead(200, { 'Content-Type': 'text/html' });
55
+ res.end('<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>');
56
+ clearTimeout(timeout);
57
+ server.close();
58
+ resolve(code);
59
+ });
60
+ server.listen(port, () => {
61
+ const authUrl = `${API_BASE}/auth/google?redirect_uri=${encodeURIComponent(redirectUri)}`;
62
+ console.log(chalk.dim('Opening browser for authentication...'));
63
+ openBrowser(authUrl);
64
+ console.log(chalk.dim(`If the browser doesn't open, visit: ${authUrl}`));
65
+ });
66
+ });
67
+ console.log(chalk.dim('Exchanging authorization code...'));
68
+ const response = await fetch(`${API_BASE}/auth/token`, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ code: authCode, redirect_uri: redirectUri }),
72
+ });
73
+ if (!response.ok) {
74
+ const error = (await response.json());
75
+ if (response.status === 403) {
76
+ console.error(chalk.red('Only @meltstudio.co accounts can use this tool.'));
77
+ process.exit(1);
78
+ }
79
+ console.error(chalk.red(`Authentication failed: ${error.error ?? 'Unknown error'}`));
80
+ process.exit(1);
81
+ }
82
+ const data = (await response.json());
83
+ await storeAuth({
84
+ token: data.token,
85
+ email: data.email,
86
+ expiresAt: data.expiresAt,
87
+ });
88
+ console.log();
89
+ console.log(chalk.green(`Logged in as ${data.email}`));
90
+ }
@@ -0,0 +1 @@
1
+ export declare function logoutCommand(): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import chalk from 'chalk';
2
+ import { clearAuth, getStoredAuth } from '../utils/auth.js';
3
+ export async function logoutCommand() {
4
+ const auth = await getStoredAuth();
5
+ await clearAuth();
6
+ if (auth) {
7
+ console.log(chalk.green(`Logged out (was ${auth.email})`));
8
+ }
9
+ else {
10
+ console.log(chalk.dim('No active session found.'));
11
+ }
12
+ }
package/dist/index.js CHANGED
@@ -3,9 +3,10 @@ import { Command } from '@commander-js/extra-typings';
3
3
  import { readFileSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { initCommand } from './commands/project/init.js';
7
- import { updateCommand } from './commands/project/update.js';
8
- import { cleanCommand } from './commands/project/clean.js';
6
+ import { initCommand } from './commands/init.js';
7
+ import { loginCommand } from './commands/login.js';
8
+ import { logoutCommand } from './commands/logout.js';
9
+ import { printBanner } from './utils/banner.js';
9
10
  import { checkAndEnforceUpdate } from './utils/version-check.js';
10
11
  import { versionCheckCommand } from './commands/version.js';
11
12
  // Read version from package.json
@@ -15,43 +16,39 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'),
15
16
  const program = new Command();
16
17
  program
17
18
  .name('meltctl')
18
- .description('CLI tool for Melt development process automation')
19
+ .description('Set up AI-first development standards (AGENTS.md, Claude skills, Cursor rules, MCP config) in your project.\n\nRequires a @meltstudio.co Google Workspace account. Run `meltctl login` first, then `meltctl project init` in your repo.')
19
20
  .version(packageJson.version)
21
+ .addHelpText('beforeAll', () => {
22
+ printBanner();
23
+ return '';
24
+ })
20
25
  .hook('preAction', async () => {
21
26
  await checkAndEnforceUpdate();
22
27
  });
23
- // Project management commands
24
- const projectCommand = program
25
- .command('project')
26
- .description('Project setup and management commands');
27
- projectCommand
28
- .command('init')
29
- .description('Initialize project with Melt development tools')
30
- .option('--shell <type>', 'specify shell type (sh|ps)', 'auto')
31
- .action(options => {
32
- // Validate shell option
33
- const validShells = ['sh', 'ps', 'auto'];
34
- const shell = validShells.includes(options.shell)
35
- ? options.shell
36
- : 'auto';
37
- return initCommand({ shell });
28
+ program
29
+ .command('login')
30
+ .description('authenticate with Google Workspace (opens browser)')
31
+ .action(async () => {
32
+ await loginCommand();
38
33
  });
39
- projectCommand
40
- .command('update')
41
- .description('Update project configurations to latest version')
42
- .action(updateCommand);
43
- projectCommand
44
- .command('clean')
45
- .description('Remove all Melt-generated files from the project')
46
- .option('-y, --yes', 'skip confirmation prompt')
34
+ program
35
+ .command('logout')
36
+ .description('clear stored credentials from ~/.meltctl/')
37
+ .action(async () => {
38
+ await logoutCommand();
39
+ });
40
+ const project = program.command('project').description('manage project configuration');
41
+ project
42
+ .command('init')
43
+ .description('scaffold Melt development tools into the current directory (AGENTS.md, .claude/, .cursor/, .mcp.json)')
44
+ .option('--force', 'overwrite existing files if already initialized')
47
45
  .action(options => {
48
- return cleanCommand({ yes: options.yes });
46
+ return initCommand({ force: options.force });
49
47
  });
50
- // Version check command
51
48
  program
52
49
  .command('version')
53
- .description('Display version information')
54
- .option('--check', 'check for updates')
50
+ .description('show current version')
51
+ .option('--check', 'also check for available updates')
55
52
  .action(async (options) => {
56
53
  if (options.check) {
57
54
  await versionCheckCommand();
@@ -0,0 +1,12 @@
1
+ export declare const API_BASE: string;
2
+ interface StoredAuth {
3
+ token: string;
4
+ email: string;
5
+ expiresAt: string;
6
+ }
7
+ export declare function getStoredAuth(): Promise<StoredAuth | undefined>;
8
+ export declare function storeAuth(auth: StoredAuth): Promise<void>;
9
+ export declare function clearAuth(): Promise<void>;
10
+ export declare function isAuthenticated(): Promise<boolean>;
11
+ export declare function authenticatedFetch(urlPath: string): Promise<Response>;
12
+ export {};
@@ -0,0 +1,52 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const AUTH_DIR = path.join(os.homedir(), '.meltctl');
5
+ const AUTH_FILE = path.join(AUTH_DIR, 'auth.json');
6
+ export const API_BASE = process.env['MELTCTL_API_URL'] ??
7
+ 'https://ewszkw32he2ebgkwlbwmoubf5y0hllpx.lambda-url.us-east-1.on.aws';
8
+ export async function getStoredAuth() {
9
+ if (!(await fs.pathExists(AUTH_FILE))) {
10
+ return undefined;
11
+ }
12
+ try {
13
+ return (await fs.readJson(AUTH_FILE));
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
19
+ export async function storeAuth(auth) {
20
+ await fs.ensureDir(AUTH_DIR);
21
+ await fs.writeJson(AUTH_FILE, auth, { spaces: 2 });
22
+ }
23
+ export async function clearAuth() {
24
+ if (await fs.pathExists(AUTH_FILE)) {
25
+ await fs.remove(AUTH_FILE);
26
+ }
27
+ }
28
+ export async function isAuthenticated() {
29
+ const auth = await getStoredAuth();
30
+ if (!auth) {
31
+ return false;
32
+ }
33
+ return new Date(auth.expiresAt) > new Date();
34
+ }
35
+ export async function authenticatedFetch(urlPath) {
36
+ const auth = await getStoredAuth();
37
+ if (!auth) {
38
+ throw new Error('Not authenticated. Run `meltctl login` first.');
39
+ }
40
+ if (new Date(auth.expiresAt) <= new Date()) {
41
+ throw new Error('Session expired. Run `meltctl login` to re-authenticate.');
42
+ }
43
+ const response = await fetch(`${API_BASE}${urlPath}`, {
44
+ headers: {
45
+ Authorization: `Bearer ${auth.token}`,
46
+ },
47
+ });
48
+ if (response.status === 401) {
49
+ throw new Error('Session expired. Run `meltctl login` to re-authenticate.');
50
+ }
51
+ return response;
52
+ }
@@ -0,0 +1 @@
1
+ export declare function printBanner(): void;
@@ -0,0 +1,22 @@
1
+ import chalk from 'chalk';
2
+ import gradient from 'gradient-string';
3
+ import { readFileSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
8
+ // Melt brand colors: yellow, magenta/pink, cyan, red, orange
9
+ const meltGradient = gradient(['#FFC107', '#E91E63', '#00BCD4', '#FF5722']);
10
+ const LOGO = `
11
+ ███╗ ███╗███████╗██╗ ████████╗ ██████╗████████╗██╗
12
+ ████╗ ████║██╔════╝██║ ╚══██╔══╝██╔════╝╚══██╔══╝██║
13
+ ██╔████╔██║█████╗ ██║ ██║ ██║ ██║ ██║
14
+ ██║╚██╔╝██║██╔══╝ ██║ ██║ ██║ ██║ ██║
15
+ ██║ ╚═╝ ██║███████╗███████╗██║ ╚██████╗ ██║ ███████╗
16
+ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚══════╝`;
17
+ export function printBanner() {
18
+ console.log(meltGradient(LOGO));
19
+ console.log();
20
+ console.log(` ${chalk.dim('v' + pkg.version)} ${chalk.dim('·')} ${chalk.dim('AI-first development tools for teams')}`);
21
+ console.log();
22
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "2.4.1",
4
- "description": "CLI tool for Melt development process automation - initialize and update project configurations",
3
+ "version": "4.0.0",
4
+ "description": "AI-first development tools for teams - set up AGENTS.md, Claude Code, Cursor, and Copilot standards",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "bin": {
@@ -9,7 +9,6 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/**/*",
12
- "templates/**/*",
13
12
  "README.md",
14
13
  "LICENSE"
15
14
  ],
@@ -37,7 +36,10 @@
37
36
  "keywords": [
38
37
  "cli",
39
38
  "development-tools",
39
+ "agents-md",
40
+ "claude-code",
40
41
  "cursor",
42
+ "copilot",
41
43
  "ai-development",
42
44
  "melt",
43
45
  "automation"
@@ -45,12 +47,11 @@
45
47
  "author": "Melt Studio",
46
48
  "license": "MIT",
47
49
  "dependencies": {
48
- "@clack/prompts": "^0.9.0",
49
- "commander": "^12.1.0",
50
50
  "@commander-js/extra-typings": "^12.1.0",
51
51
  "chalk": "^5.4.1",
52
- "ora": "^8.2.0",
53
- "fs-extra": "^11.2.0"
52
+ "commander": "^12.1.0",
53
+ "fs-extra": "^11.2.0",
54
+ "gradient-string": "^3.0.0"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/fs-extra": "^11.0.4",
@@ -1,5 +0,0 @@
1
- interface CleanOptions {
2
- yes?: boolean;
3
- }
4
- export declare function cleanCommand(options?: CleanOptions): Promise<void>;
5
- export {};