@meltstudio/meltctl 4.37.0 → 4.38.1
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/dist/index.js +2178 -210
- package/package.json +4 -3
- package/dist/commands/audit.d.ts +0 -10
- package/dist/commands/audit.js +0 -191
- package/dist/commands/audit.test.d.ts +0 -1
- package/dist/commands/audit.test.js +0 -324
- package/dist/commands/coins.d.ts +0 -5
- package/dist/commands/coins.js +0 -51
- package/dist/commands/coins.test.d.ts +0 -1
- package/dist/commands/coins.test.js +0 -113
- package/dist/commands/feedback.d.ts +0 -7
- package/dist/commands/feedback.js +0 -90
- package/dist/commands/feedback.test.d.ts +0 -1
- package/dist/commands/feedback.test.js +0 -177
- package/dist/commands/init.d.ts +0 -8
- package/dist/commands/init.js +0 -520
- package/dist/commands/init.test.d.ts +0 -1
- package/dist/commands/init.test.js +0 -478
- package/dist/commands/login.d.ts +0 -1
- package/dist/commands/login.js +0 -90
- package/dist/commands/login.test.d.ts +0 -1
- package/dist/commands/login.test.js +0 -194
- package/dist/commands/logout.d.ts +0 -1
- package/dist/commands/logout.js +0 -12
- package/dist/commands/logout.test.d.ts +0 -1
- package/dist/commands/logout.test.js +0 -59
- package/dist/commands/plan.d.ts +0 -6
- package/dist/commands/plan.js +0 -123
- package/dist/commands/plan.test.d.ts +0 -1
- package/dist/commands/plan.test.js +0 -246
- package/dist/commands/standup.d.ts +0 -7
- package/dist/commands/standup.js +0 -74
- package/dist/commands/standup.test.d.ts +0 -1
- package/dist/commands/standup.test.js +0 -218
- package/dist/commands/templates.d.ts +0 -1
- package/dist/commands/templates.js +0 -37
- package/dist/commands/templates.test.d.ts +0 -1
- package/dist/commands/templates.test.js +0 -89
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.js +0 -74
- package/dist/commands/update.test.d.ts +0 -1
- package/dist/commands/update.test.js +0 -93
- package/dist/commands/version.d.ts +0 -1
- package/dist/commands/version.js +0 -43
- package/dist/commands/version.test.d.ts +0 -1
- package/dist/commands/version.test.js +0 -86
- package/dist/index.d.ts +0 -2
- package/dist/utils/analytics.d.ts +0 -1
- package/dist/utils/analytics.js +0 -54
- package/dist/utils/analytics.test.d.ts +0 -1
- package/dist/utils/analytics.test.js +0 -91
- package/dist/utils/api.d.ts +0 -3
- package/dist/utils/api.js +0 -23
- package/dist/utils/api.test.d.ts +0 -1
- package/dist/utils/api.test.js +0 -76
- package/dist/utils/auth.d.ts +0 -12
- package/dist/utils/auth.js +0 -54
- package/dist/utils/auth.test.d.ts +0 -1
- package/dist/utils/auth.test.js +0 -165
- package/dist/utils/banner.d.ts +0 -1
- package/dist/utils/banner.js +0 -22
- package/dist/utils/banner.test.d.ts +0 -1
- package/dist/utils/banner.test.js +0 -34
- package/dist/utils/debug.d.ts +0 -1
- package/dist/utils/debug.js +0 -6
- package/dist/utils/git.d.ts +0 -9
- package/dist/utils/git.js +0 -76
- package/dist/utils/git.test.d.ts +0 -1
- package/dist/utils/git.test.js +0 -184
- package/dist/utils/package-manager.d.ts +0 -7
- package/dist/utils/package-manager.js +0 -55
- package/dist/utils/package-manager.test.d.ts +0 -1
- package/dist/utils/package-manager.test.js +0 -76
- package/dist/utils/templates.d.ts +0 -2
- package/dist/utils/templates.js +0 -5
- package/dist/utils/templates.test.d.ts +0 -1
- package/dist/utils/templates.test.js +0 -38
- package/dist/utils/version-check.d.ts +0 -7
- package/dist/utils/version-check.js +0 -139
- package/dist/utils/version-check.test.d.ts +0 -1
- package/dist/utils/version-check.test.js +0 -189
package/dist/index.js
CHANGED
|
@@ -1,217 +1,2185 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/utils/version.ts
|
|
13
|
+
var CLI_VERSION;
|
|
14
|
+
var init_version = __esm({
|
|
15
|
+
"src/utils/version.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
CLI_VERSION = "4.38.1";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/commands/update.ts
|
|
22
|
+
var update_exports = {};
|
|
23
|
+
__export(update_exports, {
|
|
24
|
+
detectPackageManager: () => detectPackageManager,
|
|
25
|
+
updateCommand: () => updateCommand
|
|
26
|
+
});
|
|
27
|
+
import chalk7 from "chalk";
|
|
28
|
+
import { execSync } from "child_process";
|
|
29
|
+
import path4 from "path";
|
|
30
|
+
import { fileURLToPath } from "url";
|
|
31
|
+
function detectPackageManager() {
|
|
32
|
+
const installPath = path4.resolve(__dirname, "../..");
|
|
33
|
+
try {
|
|
34
|
+
const npmGlobalPrefix = execSync("npm prefix -g", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
35
|
+
if (installPath.startsWith(npmGlobalPrefix)) {
|
|
36
|
+
return "npm";
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const yarnGlobalDir = execSync("yarn global dir", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
42
|
+
if (installPath.startsWith(yarnGlobalDir)) {
|
|
43
|
+
return "yarn";
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
if (installPath.includes("yarn")) return "yarn";
|
|
48
|
+
if (installPath.includes("npm")) return "npm";
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
async function updateCommand() {
|
|
52
|
+
const currentVersion = await getCurrentCliVersion();
|
|
53
|
+
const latestVersion = await getLatestCliVersion();
|
|
54
|
+
if (!latestVersion) {
|
|
55
|
+
console.error(chalk7.red("Could not check for updates. Check your network connection."));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const severity = getUpdateSeverity(currentVersion, latestVersion);
|
|
59
|
+
if (severity === "none") {
|
|
60
|
+
console.log(chalk7.green(` \u2713 Already on the latest version (${currentVersion})`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log(chalk7.dim(` ${currentVersion} \u2192 ${latestVersion}`));
|
|
64
|
+
console.log();
|
|
65
|
+
const pm = detectPackageManager();
|
|
66
|
+
let cmd;
|
|
67
|
+
if (pm === "yarn") {
|
|
68
|
+
cmd = "yarn global add @meltstudio/meltctl@latest";
|
|
69
|
+
} else if (pm === "npm") {
|
|
70
|
+
cmd = "npm install -g @meltstudio/meltctl@latest";
|
|
71
|
+
} else {
|
|
72
|
+
console.log(chalk7.dim(" Could not detect package manager, trying npm..."));
|
|
73
|
+
cmd = "npm install -g @meltstudio/meltctl@latest";
|
|
74
|
+
}
|
|
75
|
+
console.log(chalk7.dim(` Running: ${cmd}`));
|
|
76
|
+
console.log();
|
|
77
|
+
try {
|
|
78
|
+
execSync(cmd, { stdio: "inherit" });
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(chalk7.green(` \u2713 Updated to ${latestVersion}`));
|
|
81
|
+
} catch {
|
|
82
|
+
console.error();
|
|
83
|
+
console.error(chalk7.red(" Update failed. Try running manually:"));
|
|
84
|
+
console.error(chalk7.cyan(` ${cmd}`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
var __dirname;
|
|
89
|
+
var init_update = __esm({
|
|
90
|
+
"src/commands/update.ts"() {
|
|
91
|
+
"use strict";
|
|
92
|
+
init_version_check();
|
|
93
|
+
__dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// src/utils/version-check.ts
|
|
98
|
+
import chalk8 from "chalk";
|
|
99
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
100
|
+
import { execSync as execSync2 } from "child_process";
|
|
101
|
+
async function getCurrentCliVersion() {
|
|
102
|
+
return CLI_VERSION;
|
|
103
|
+
}
|
|
104
|
+
async function getLatestCliVersion() {
|
|
105
|
+
try {
|
|
106
|
+
const result = execSync2("npm view @meltstudio/meltctl version --json", {
|
|
107
|
+
encoding: "utf-8",
|
|
108
|
+
stdio: "pipe",
|
|
109
|
+
timeout: 5e3
|
|
110
|
+
// 5 second timeout
|
|
111
|
+
});
|
|
112
|
+
return JSON.parse(result.trim());
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function compareVersions(current, latest) {
|
|
118
|
+
const parseVersion = (version) => {
|
|
119
|
+
const [base, prerelease] = version.split("-");
|
|
120
|
+
const parts = (base || "").split(".").map(Number);
|
|
121
|
+
return {
|
|
122
|
+
major: parts[0] || 0,
|
|
123
|
+
minor: parts[1] || 0,
|
|
124
|
+
patch: parts[2] || 0,
|
|
125
|
+
prerelease: prerelease || null
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const currentVer = parseVersion(current);
|
|
129
|
+
const latestVer = parseVersion(latest);
|
|
130
|
+
if (latestVer.major !== currentVer.major) {
|
|
131
|
+
return latestVer.major > currentVer.major;
|
|
132
|
+
}
|
|
133
|
+
if (latestVer.minor !== currentVer.minor) {
|
|
134
|
+
return latestVer.minor > currentVer.minor;
|
|
135
|
+
}
|
|
136
|
+
if (latestVer.patch !== currentVer.patch) {
|
|
137
|
+
return latestVer.patch > currentVer.patch;
|
|
138
|
+
}
|
|
139
|
+
if (!latestVer.prerelease && currentVer.prerelease) return true;
|
|
140
|
+
if (latestVer.prerelease && !currentVer.prerelease) return false;
|
|
141
|
+
if (latestVer.prerelease && currentVer.prerelease) {
|
|
142
|
+
return latestVer.prerelease > currentVer.prerelease;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function isCI() {
|
|
147
|
+
return !!(process.env.CI || // Generic CI flag
|
|
148
|
+
process.env.GITHUB_ACTIONS || // GitHub Actions
|
|
149
|
+
process.env.GITLAB_CI || // GitLab CI
|
|
150
|
+
process.env.CIRCLECI || // CircleCI
|
|
151
|
+
process.env.TRAVIS || // Travis CI
|
|
152
|
+
process.env.JENKINS_URL || // Jenkins
|
|
153
|
+
process.env.BUILDKITE || // Buildkite
|
|
154
|
+
process.env.DRONE || // Drone
|
|
155
|
+
process.env.MELTCTL_SKIP_UPDATE_CHECK);
|
|
156
|
+
}
|
|
157
|
+
function getUpdateSeverity(current, latest) {
|
|
158
|
+
const parseCurrent = current.split("-")[0].split(".").map(Number);
|
|
159
|
+
const parseLatest = latest.split("-")[0].split(".").map(Number);
|
|
160
|
+
const [curMajor = 0, curMinor = 0, curPatch = 0] = parseCurrent;
|
|
161
|
+
const [latMajor = 0, latMinor = 0, latPatch = 0] = parseLatest;
|
|
162
|
+
if (latMajor > curMajor) return "major";
|
|
163
|
+
if (latMajor === curMajor && latMinor > curMinor) return "minor";
|
|
164
|
+
if (latMajor === curMajor && latMinor === curMinor && latPatch > curPatch) return "patch";
|
|
165
|
+
return "none";
|
|
166
|
+
}
|
|
167
|
+
async function checkAndEnforceUpdate() {
|
|
168
|
+
if (isCI()) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const currentVersion = await getCurrentCliVersion();
|
|
173
|
+
const latestVersion = await getLatestCliVersion();
|
|
174
|
+
if (!latestVersion) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const severity = getUpdateSeverity(currentVersion, latestVersion);
|
|
178
|
+
if (severity === "none") {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (severity === "patch") {
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(
|
|
184
|
+
chalk8.yellow(
|
|
185
|
+
` Update available: ${currentVersion} \u2192 ${latestVersion} (run: meltctl update)`
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
console.log();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(chalk8.yellow(` Update required: ${currentVersion} \u2192 ${latestVersion}`));
|
|
193
|
+
console.log();
|
|
194
|
+
const shouldUpdate = await confirm2({
|
|
195
|
+
message: "Update now?",
|
|
196
|
+
default: true
|
|
197
|
+
});
|
|
198
|
+
if (shouldUpdate) {
|
|
199
|
+
const { updateCommand: updateCommand2 } = await Promise.resolve().then(() => (init_update(), update_exports));
|
|
200
|
+
await updateCommand2();
|
|
201
|
+
console.log();
|
|
202
|
+
console.log(chalk8.dim(" Please re-run your command."));
|
|
203
|
+
console.log();
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
console.log();
|
|
207
|
+
console.log(chalk8.gray("To skip this check (CI/CD), set MELTCTL_SKIP_UPDATE_CHECK=1"));
|
|
208
|
+
console.log();
|
|
209
|
+
process.exit(1);
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
var init_version_check = __esm({
|
|
215
|
+
"src/utils/version-check.ts"() {
|
|
216
|
+
"use strict";
|
|
217
|
+
init_version();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// src/index.ts
|
|
222
|
+
import chalk16 from "chalk";
|
|
223
|
+
import { Command, Option } from "@commander-js/extra-typings";
|
|
224
|
+
|
|
225
|
+
// src/commands/init.ts
|
|
226
|
+
import chalk2 from "chalk";
|
|
227
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
228
|
+
import fs2 from "fs-extra";
|
|
229
|
+
import path2 from "path";
|
|
230
|
+
|
|
231
|
+
// src/utils/auth.ts
|
|
232
|
+
import fs from "fs-extra";
|
|
233
|
+
import path from "path";
|
|
234
|
+
import os from "os";
|
|
235
|
+
var AUTH_DIR = path.join(os.homedir(), ".meltctl");
|
|
236
|
+
var AUTH_FILE = path.join(AUTH_DIR, "auth.json");
|
|
237
|
+
var API_BASE = process.env["MELTCTL_API_URL"] ?? "https://ewszkw32he2ebgkwlbwmoubf5y0hllpx.lambda-url.us-east-1.on.aws";
|
|
238
|
+
async function getStoredAuth() {
|
|
239
|
+
if (!await fs.pathExists(AUTH_FILE)) {
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
return await fs.readJson(AUTH_FILE);
|
|
244
|
+
} catch {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function storeAuth(auth) {
|
|
249
|
+
await fs.ensureDir(AUTH_DIR);
|
|
250
|
+
await fs.writeJson(AUTH_FILE, auth, { spaces: 2 });
|
|
251
|
+
}
|
|
252
|
+
async function clearAuth() {
|
|
253
|
+
if (await fs.pathExists(AUTH_FILE)) {
|
|
254
|
+
await fs.remove(AUTH_FILE);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function isAuthenticated() {
|
|
258
|
+
const auth = await getStoredAuth();
|
|
259
|
+
if (!auth) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return new Date(auth.expiresAt) > /* @__PURE__ */ new Date();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/utils/api.ts
|
|
266
|
+
import chalk from "chalk";
|
|
267
|
+
|
|
268
|
+
// ../sdk/dist/resources/audits.js
|
|
269
|
+
function createAuditsResource(config) {
|
|
270
|
+
return {
|
|
271
|
+
async submit(input3) {
|
|
272
|
+
const { data, status } = await apiFetch(config, "/audits", {
|
|
273
|
+
method: "POST",
|
|
274
|
+
body: JSON.stringify(input3)
|
|
275
|
+
});
|
|
276
|
+
if (status !== 201)
|
|
277
|
+
throw new Error(data.error ?? `Failed to submit audit (${status})`);
|
|
278
|
+
return data;
|
|
279
|
+
},
|
|
280
|
+
async list(filters) {
|
|
281
|
+
const params = new URLSearchParams();
|
|
282
|
+
if (filters?.type)
|
|
283
|
+
params.set("type", filters.type);
|
|
284
|
+
if (filters?.repository)
|
|
285
|
+
params.set("repository", filters.repository);
|
|
286
|
+
if (filters?.latest)
|
|
287
|
+
params.set("latest", "true");
|
|
288
|
+
if (filters?.limit)
|
|
289
|
+
params.set("limit", String(filters.limit));
|
|
290
|
+
const query = params.toString();
|
|
291
|
+
const path8 = `/audits${query ? `?${query}` : ""}`;
|
|
292
|
+
const { data, status } = await apiFetch(config, path8);
|
|
293
|
+
if (status === 403)
|
|
294
|
+
throw new Error("Access denied. Only Team Managers can list audits.");
|
|
295
|
+
if (status !== 200)
|
|
296
|
+
throw new Error(data.error ?? `Failed to list audits (${status})`);
|
|
297
|
+
return data;
|
|
298
|
+
},
|
|
299
|
+
async get(id) {
|
|
300
|
+
const { data, status } = await apiFetch(config, `/audits/${id}`);
|
|
301
|
+
if (status === 403)
|
|
302
|
+
throw new Error("Access denied. Only Team Managers can view audits.");
|
|
303
|
+
if (status === 404)
|
|
304
|
+
throw new Error(`Audit not found: ${id}`);
|
|
305
|
+
if (status !== 200)
|
|
306
|
+
throw new Error(data.error ?? `Failed to fetch audit (${status})`);
|
|
307
|
+
return data;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ../sdk/dist/resources/plans.js
|
|
313
|
+
function createPlansResource(config) {
|
|
314
|
+
return {
|
|
315
|
+
async submit(input3) {
|
|
316
|
+
const { data, status } = await apiFetch(config, "/plans", {
|
|
317
|
+
method: "POST",
|
|
318
|
+
body: JSON.stringify(input3)
|
|
319
|
+
});
|
|
320
|
+
if (status !== 201 && status !== 200)
|
|
321
|
+
throw new Error(data.error ?? `Failed to submit plan (${status})`);
|
|
322
|
+
return data;
|
|
323
|
+
},
|
|
324
|
+
async list(filters) {
|
|
325
|
+
const params = new URLSearchParams();
|
|
326
|
+
if (filters?.repository)
|
|
327
|
+
params.set("repository", filters.repository);
|
|
328
|
+
if (filters?.author)
|
|
329
|
+
params.set("author", filters.author);
|
|
330
|
+
if (filters?.ticket)
|
|
331
|
+
params.set("ticket", filters.ticket);
|
|
332
|
+
if (filters?.status)
|
|
333
|
+
params.set("status", filters.status);
|
|
334
|
+
if (filters?.limit)
|
|
335
|
+
params.set("limit", String(filters.limit));
|
|
336
|
+
const query = params.toString();
|
|
337
|
+
const path8 = `/plans${query ? `?${query}` : ""}`;
|
|
338
|
+
const { data, status } = await apiFetch(config, path8);
|
|
339
|
+
if (status === 403)
|
|
340
|
+
throw new Error("Access denied. Only Team Managers can list plans.");
|
|
341
|
+
if (status !== 200)
|
|
342
|
+
throw new Error(data.error ?? `Failed to list plans (${status})`);
|
|
343
|
+
return data;
|
|
344
|
+
},
|
|
345
|
+
async get(id) {
|
|
346
|
+
const { data, status } = await apiFetch(config, `/plans/${id}`);
|
|
347
|
+
if (status === 403)
|
|
348
|
+
throw new Error("Access denied. Only Team Managers can view plans.");
|
|
349
|
+
if (status === 404)
|
|
350
|
+
throw new Error(`Plan not found: ${id}`);
|
|
351
|
+
if (status !== 200)
|
|
352
|
+
throw new Error(data.error ?? `Failed to fetch plan (${status})`);
|
|
353
|
+
return data;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ../sdk/dist/resources/events.js
|
|
359
|
+
function createEventsResource(config) {
|
|
360
|
+
return {
|
|
361
|
+
async submit(input3) {
|
|
362
|
+
const { data, status } = await apiFetch(config, "/events", {
|
|
363
|
+
method: "POST",
|
|
364
|
+
body: JSON.stringify(input3)
|
|
365
|
+
});
|
|
366
|
+
if (status !== 201)
|
|
367
|
+
throw new Error(data.error ?? `Failed to submit event (${status})`);
|
|
368
|
+
return data;
|
|
369
|
+
},
|
|
370
|
+
async list(filters) {
|
|
371
|
+
const params = new URLSearchParams();
|
|
372
|
+
if (filters?.command)
|
|
373
|
+
params.set("command", filters.command);
|
|
374
|
+
if (filters?.author)
|
|
375
|
+
params.set("author", filters.author);
|
|
376
|
+
if (filters?.repository)
|
|
377
|
+
params.set("repository", filters.repository);
|
|
378
|
+
if (filters?.dateFrom)
|
|
379
|
+
params.set("dateFrom", filters.dateFrom);
|
|
380
|
+
if (filters?.dateTo)
|
|
381
|
+
params.set("dateTo", filters.dateTo);
|
|
382
|
+
if (filters?.limit)
|
|
383
|
+
params.set("limit", String(filters.limit));
|
|
384
|
+
const query = params.toString();
|
|
385
|
+
const path8 = `/events${query ? `?${query}` : ""}`;
|
|
386
|
+
const { data, status } = await apiFetch(config, path8);
|
|
387
|
+
if (status === 403)
|
|
388
|
+
throw new Error("Access denied. Only Team Managers can view analytics.");
|
|
389
|
+
if (status !== 200)
|
|
390
|
+
throw new Error(data.error ?? `Failed to list events (${status})`);
|
|
391
|
+
return data;
|
|
392
|
+
},
|
|
393
|
+
async getStats(filters) {
|
|
394
|
+
const params = new URLSearchParams();
|
|
395
|
+
if (filters?.dateFrom)
|
|
396
|
+
params.set("dateFrom", filters.dateFrom);
|
|
397
|
+
if (filters?.dateTo)
|
|
398
|
+
params.set("dateTo", filters.dateTo);
|
|
399
|
+
const query = params.toString();
|
|
400
|
+
const path8 = `/events/stats${query ? `?${query}` : ""}`;
|
|
401
|
+
const { data, status } = await apiFetch(config, path8);
|
|
402
|
+
if (status === 403)
|
|
403
|
+
throw new Error("Access denied. Only Team Managers can view analytics.");
|
|
404
|
+
if (status !== 200)
|
|
405
|
+
throw new Error(data.error ?? `Failed to fetch stats (${status})`);
|
|
406
|
+
return data;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ../sdk/dist/resources/standups.js
|
|
412
|
+
function createStandupsResource(config) {
|
|
413
|
+
return {
|
|
414
|
+
async submit(input3) {
|
|
415
|
+
const { data, status } = await apiFetch(config, "/standups", {
|
|
416
|
+
method: "POST",
|
|
417
|
+
body: JSON.stringify(input3)
|
|
418
|
+
});
|
|
419
|
+
if (status !== 201)
|
|
420
|
+
throw new Error(data.error ?? `Failed to submit standup (${status})`);
|
|
421
|
+
return data;
|
|
422
|
+
},
|
|
423
|
+
async getStatus() {
|
|
424
|
+
const { data, status } = await apiFetch(config, "/standups/status");
|
|
425
|
+
if (status === 404)
|
|
426
|
+
return null;
|
|
427
|
+
if (status !== 200)
|
|
428
|
+
throw new Error(data.error ?? `Failed to check standup status (${status})`);
|
|
429
|
+
return data;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ../sdk/dist/resources/feedback.js
|
|
435
|
+
function createFeedbackResource(config) {
|
|
436
|
+
return {
|
|
437
|
+
async submit(input3) {
|
|
438
|
+
const { data, status } = await apiFetch(config, "/feedback", {
|
|
439
|
+
method: "POST",
|
|
440
|
+
body: JSON.stringify(input3)
|
|
441
|
+
});
|
|
442
|
+
if (status !== 201)
|
|
443
|
+
throw new Error(data.error ?? `Failed to send feedback (${status})`);
|
|
444
|
+
return data;
|
|
445
|
+
},
|
|
446
|
+
async getRecipients() {
|
|
447
|
+
const { data, status } = await apiFetch(config, "/feedback/recipients");
|
|
448
|
+
if (status !== 200)
|
|
449
|
+
throw new Error(`Failed to load recipients (${status})`);
|
|
450
|
+
return data;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ../sdk/dist/resources/coins.js
|
|
456
|
+
function createCoinsResource(config) {
|
|
457
|
+
return {
|
|
458
|
+
async getBalance() {
|
|
459
|
+
const { data, status } = await apiFetch(config, "/coins");
|
|
460
|
+
if (status !== 200)
|
|
461
|
+
throw new Error(data.error ?? `Failed to fetch coins (${status})`);
|
|
462
|
+
return data;
|
|
463
|
+
},
|
|
464
|
+
async getLeaderboard() {
|
|
465
|
+
const { data, status } = await apiFetch(config, "/coins/leaderboard");
|
|
466
|
+
if (status !== 200)
|
|
467
|
+
throw new Error(`Failed to fetch leaderboard (${status})`);
|
|
468
|
+
return data;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ../sdk/dist/resources/templates.js
|
|
474
|
+
function createTemplatesResource(config) {
|
|
475
|
+
return {
|
|
476
|
+
async fetch() {
|
|
477
|
+
const { data, status } = await apiFetch(config, "/templates");
|
|
478
|
+
if (status !== 200)
|
|
479
|
+
throw new Error(data.error ?? `Failed to fetch templates (${status})`);
|
|
480
|
+
return data.files;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ../sdk/dist/resources/auth.js
|
|
486
|
+
function createAuthResource(config) {
|
|
487
|
+
return {
|
|
488
|
+
async exchangeToken(code, redirectUri) {
|
|
489
|
+
const response = await fetch(`${config.baseUrl}/auth/token`, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: { "Content-Type": "application/json" },
|
|
492
|
+
body: JSON.stringify({ code, redirect_uri: redirectUri })
|
|
493
|
+
});
|
|
494
|
+
const data = await response.json();
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
if (response.status === 403)
|
|
497
|
+
throw new Error("Only @meltstudio.co accounts can use this tool.");
|
|
498
|
+
throw new Error(data.error ?? `Authentication failed (${response.status})`);
|
|
499
|
+
}
|
|
500
|
+
return data;
|
|
501
|
+
},
|
|
502
|
+
async createCiToken() {
|
|
503
|
+
const response = await fetch(`${config.baseUrl}/auth/create-ci-token`, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: {
|
|
506
|
+
Authorization: `Bearer ${config.token}`,
|
|
507
|
+
"Content-Type": "application/json"
|
|
59
508
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
509
|
+
});
|
|
510
|
+
const data = await response.json();
|
|
511
|
+
if (!response.ok)
|
|
512
|
+
throw new Error(data.error ?? `Failed to create CI token (${response.status})`);
|
|
513
|
+
return data;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ../sdk/dist/resources/people.js
|
|
519
|
+
function createPeopleResource(config) {
|
|
520
|
+
return {
|
|
521
|
+
async list() {
|
|
522
|
+
const { data, status } = await apiFetch(config, "/people");
|
|
523
|
+
if (status === 403)
|
|
524
|
+
throw new Error("Access denied. Only Team Managers can view people.");
|
|
525
|
+
if (status !== 200)
|
|
526
|
+
throw new Error(`Failed to fetch people (${status})`);
|
|
527
|
+
return data;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ../sdk/dist/client.js
|
|
533
|
+
async function apiFetch(config, path8, options = {}) {
|
|
534
|
+
const response = await fetch(`${config.baseUrl}${path8}`, {
|
|
535
|
+
...options,
|
|
536
|
+
headers: {
|
|
537
|
+
Authorization: `Bearer ${config.token}`,
|
|
538
|
+
"Content-Type": "application/json",
|
|
539
|
+
...options.headers
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
const data = await response.json();
|
|
543
|
+
return { data, status: response.status };
|
|
544
|
+
}
|
|
545
|
+
function createMeltClient(config) {
|
|
546
|
+
return {
|
|
547
|
+
audits: createAuditsResource(config),
|
|
548
|
+
plans: createPlansResource(config),
|
|
549
|
+
events: createEventsResource(config),
|
|
550
|
+
standups: createStandupsResource(config),
|
|
551
|
+
feedback: createFeedbackResource(config),
|
|
552
|
+
coins: createCoinsResource(config),
|
|
553
|
+
templates: createTemplatesResource(config),
|
|
554
|
+
auth: createAuthResource(config),
|
|
555
|
+
people: createPeopleResource(config)
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/utils/api.ts
|
|
560
|
+
async function getToken() {
|
|
561
|
+
const envToken = process.env["MELTCTL_TOKEN"];
|
|
562
|
+
if (envToken) {
|
|
563
|
+
return envToken;
|
|
564
|
+
}
|
|
565
|
+
const auth = await getStoredAuth();
|
|
566
|
+
if (!auth) {
|
|
567
|
+
console.error(
|
|
568
|
+
chalk.red(
|
|
569
|
+
"Not authenticated. Run `npx @meltstudio/meltctl@latest login` or set MELTCTL_TOKEN for CI."
|
|
570
|
+
)
|
|
571
|
+
);
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
if (new Date(auth.expiresAt) <= /* @__PURE__ */ new Date()) {
|
|
575
|
+
console.error(
|
|
576
|
+
chalk.red(
|
|
577
|
+
"Session expired. Run `npx @meltstudio/meltctl@latest login` to re-authenticate, or set MELTCTL_TOKEN for CI."
|
|
578
|
+
)
|
|
579
|
+
);
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
return auth.token;
|
|
583
|
+
}
|
|
584
|
+
async function getClient() {
|
|
585
|
+
const token = await getToken();
|
|
586
|
+
return createMeltClient({ baseUrl: API_BASE, token });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/utils/templates.ts
|
|
590
|
+
async function fetchTemplates() {
|
|
591
|
+
const client = await getClient();
|
|
592
|
+
return client.templates.fetch();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/commands/init.ts
|
|
596
|
+
var SKILL_FRONTMATTER = {
|
|
597
|
+
setup: `---
|
|
598
|
+
user-invocable: true
|
|
599
|
+
description: >-
|
|
600
|
+
Analyze the project and customize AGENTS.md for this codebase.
|
|
601
|
+
Use when setting up a new project, after running meltctl init,
|
|
602
|
+
or when AGENTS.md has placeholder markers. Detects tech stack,
|
|
603
|
+
fills in project-specific sections, and merges existing standards.
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
`,
|
|
607
|
+
plan: `---
|
|
608
|
+
user-invocable: true
|
|
609
|
+
description: >-
|
|
610
|
+
Design an implementation approach before writing code. Use when
|
|
611
|
+
starting a feature, tackling a complex task, or when the developer
|
|
612
|
+
says "plan this" or "how should we approach this". Gathers requirements,
|
|
613
|
+
explores codebase, and presents a step-by-step plan for approval.
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
`,
|
|
617
|
+
review: `---
|
|
618
|
+
user-invocable: true
|
|
619
|
+
description: >-
|
|
620
|
+
Review code changes against project standards and address PR feedback.
|
|
621
|
+
Use when the developer asks to review changes, check code quality,
|
|
622
|
+
or respond to PR reviewer comments. Categorizes findings as must-fix,
|
|
623
|
+
should-fix, or suggestions.
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
`,
|
|
627
|
+
pr: `---
|
|
628
|
+
user-invocable: true
|
|
629
|
+
description: >-
|
|
630
|
+
Create a well-structured pull request from current changes. Use when
|
|
631
|
+
the developer is ready to submit work, says "create a PR", or "open
|
|
632
|
+
a pull request". Analyzes changes, runs pre-flight checks, drafts
|
|
633
|
+
description, and creates the PR via gh CLI.
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
`,
|
|
637
|
+
debug: `---
|
|
638
|
+
user-invocable: true
|
|
639
|
+
description: >-
|
|
640
|
+
Systematically investigate and fix bugs. Use when the developer
|
|
641
|
+
reports a bug, encounters an error, or says "debug this" or "why
|
|
642
|
+
is this failing". Reproduces the issue, isolates root cause, writes
|
|
643
|
+
regression test, and implements minimal fix.
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
`,
|
|
647
|
+
audit: `---
|
|
648
|
+
user-invocable: true
|
|
649
|
+
description: >-
|
|
650
|
+
Run a comprehensive project compliance audit against team standards.
|
|
651
|
+
Use when the developer wants to assess project health, check compliance,
|
|
652
|
+
or says "audit this project". Checks 15 categories including documentation,
|
|
653
|
+
testing, CI/CD, security, and AI tool setup. Produces a structured
|
|
654
|
+
report with scores and actionable fixes.
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
`,
|
|
658
|
+
"ux-audit": `---
|
|
659
|
+
user-invocable: true
|
|
660
|
+
description: >-
|
|
661
|
+
Review the project's UI against usability heuristics using Chrome DevTools
|
|
662
|
+
MCP. Use when the developer wants to check UX quality, says "review the UI",
|
|
663
|
+
or "UX audit". In full audit mode, crawls the entire app. During an active
|
|
664
|
+
plan, scopes to the current feature and appends results to the plan file.
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
`,
|
|
668
|
+
"security-audit": `---
|
|
669
|
+
user-invocable: true
|
|
670
|
+
description: >-
|
|
671
|
+
Run a comprehensive security posture audit across the entire platform.
|
|
672
|
+
Use when the developer wants to assess security, says "security audit",
|
|
673
|
+
or "check our security posture". Covers infrastructure, encryption, auth,
|
|
674
|
+
application security, data protection, CI/CD, and compliance readiness.
|
|
675
|
+
Investigates all platform repositories for a holistic view.
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
`,
|
|
679
|
+
validate: `---
|
|
680
|
+
user-invocable: true
|
|
681
|
+
description: >-
|
|
682
|
+
Run the validation plan from the plan document after implementation.
|
|
683
|
+
Use when the developer says "validate this", "test the feature", or
|
|
684
|
+
after finishing implementation. Validates end-to-end using browser,
|
|
685
|
+
API, or CLI testing, suggests test coverage improvements, then prompts
|
|
686
|
+
the developer for mandatory manual sign-off.
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
`,
|
|
690
|
+
update: `---
|
|
691
|
+
user-invocable: true
|
|
692
|
+
description: >-
|
|
693
|
+
Update Melt skills and standards to the latest version. Use when the
|
|
694
|
+
developer wants the latest skill templates, says "update melt", or
|
|
695
|
+
after a new meltctl version is released. Fetches latest templates,
|
|
696
|
+
preserves project customizations in AGENTS.md, and merges changes.
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
`,
|
|
700
|
+
help: `---
|
|
701
|
+
user-invocable: true
|
|
702
|
+
description: >-
|
|
703
|
+
Answer questions about the AI-First Development Playbook and team
|
|
704
|
+
workflow. Use when the developer asks about the development process,
|
|
705
|
+
what step they're on, how validation works, or any workflow question.
|
|
706
|
+
This is a reference skill \u2014 it explains the process, not executes it.
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
`,
|
|
710
|
+
link: `---
|
|
711
|
+
user-invocable: true
|
|
712
|
+
description: >-
|
|
713
|
+
Connect and verify required integrations (ticket tracker, browser
|
|
714
|
+
testing). Use after melt-setup, when tools aren't working, or when
|
|
715
|
+
a skill reports missing MCP connections. Guides setup, verifies
|
|
716
|
+
each tool works, and updates AGENTS.md connection status.
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
`
|
|
720
|
+
};
|
|
721
|
+
var OPENCODE_COMMAND_FRONTMATTER = {
|
|
722
|
+
setup: `---
|
|
723
|
+
description: Analyze the project and customize AGENTS.md for this codebase.
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
`,
|
|
727
|
+
plan: `---
|
|
728
|
+
description: Design an implementation approach before writing code.
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
`,
|
|
732
|
+
review: `---
|
|
733
|
+
description: Review code changes against project standards and address PR feedback.
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
`,
|
|
737
|
+
pr: `---
|
|
738
|
+
description: Create a well-structured pull request from current changes.
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
`,
|
|
742
|
+
debug: `---
|
|
743
|
+
description: Systematically investigate and fix bugs.
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
`,
|
|
747
|
+
audit: `---
|
|
748
|
+
description: Run a comprehensive project compliance audit against team standards.
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
`,
|
|
752
|
+
"ux-audit": `---
|
|
753
|
+
description: Review the project's UI against usability heuristics using Chrome DevTools MCP.
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
`,
|
|
757
|
+
"security-audit": `---
|
|
758
|
+
description: Run a comprehensive security posture audit across the entire platform.
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
`,
|
|
762
|
+
validate: `---
|
|
763
|
+
description: Run the validation plan from the plan document after implementation.
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
`,
|
|
767
|
+
update: `---
|
|
768
|
+
description: Update Melt skills and standards to the latest version.
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
`,
|
|
772
|
+
help: `---
|
|
773
|
+
description: Answer questions about the AI-First Development Playbook and team workflow.
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
`,
|
|
777
|
+
link: `---
|
|
778
|
+
description: Connect and verify required integrations (ticket tracker, browser testing).
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
`
|
|
782
|
+
};
|
|
783
|
+
var GITIGNORE_ENTRIES = [".env.local", ".claude/settings.local.json"];
|
|
784
|
+
function detectExistingTools(cwd) {
|
|
785
|
+
return {
|
|
786
|
+
claude: fs2.pathExistsSync(path2.join(cwd, ".claude/settings.json")),
|
|
787
|
+
cursor: fs2.pathExistsSync(path2.join(cwd, ".cursor/commands")),
|
|
788
|
+
opencode: fs2.pathExistsSync(path2.join(cwd, ".opencode/commands"))
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
async function promptToolSelection(existingTools) {
|
|
792
|
+
const choices = [];
|
|
793
|
+
if (!existingTools?.claude) {
|
|
794
|
+
choices.push({ name: "Claude Code", value: "claude", checked: true });
|
|
795
|
+
}
|
|
796
|
+
if (!existingTools?.cursor) {
|
|
797
|
+
choices.push({ name: "Cursor", value: "cursor", checked: true });
|
|
798
|
+
}
|
|
799
|
+
if (!existingTools?.opencode) {
|
|
800
|
+
choices.push({ name: "OpenCode", value: "opencode", checked: true });
|
|
801
|
+
}
|
|
802
|
+
choices.push({
|
|
803
|
+
name: "Other \u2014 contact us in #dev on Slack to request support",
|
|
804
|
+
value: "other"
|
|
805
|
+
});
|
|
806
|
+
const selected = await checkbox({
|
|
807
|
+
message: "Which AI coding tools do you use?",
|
|
808
|
+
choices
|
|
809
|
+
});
|
|
810
|
+
return {
|
|
811
|
+
claude: selected.includes("claude"),
|
|
812
|
+
cursor: selected.includes("cursor"),
|
|
813
|
+
opencode: selected.includes("opencode"),
|
|
814
|
+
other: selected.includes("other")
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
async function initCommand(options) {
|
|
818
|
+
if (!await isAuthenticated()) {
|
|
819
|
+
console.error(chalk2.red("Not authenticated. Run `npx @meltstudio/meltctl@latest login` first."));
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
const cwd = process.cwd();
|
|
823
|
+
const isGitRepo = await fs2.pathExists(path2.join(cwd, ".git"));
|
|
824
|
+
if (!isGitRepo) {
|
|
825
|
+
console.log(chalk2.yellow(`Warning: ${cwd} is not a git repository.`));
|
|
826
|
+
console.log(chalk2.dim("meltctl init should be run from the root of your project."));
|
|
827
|
+
console.log();
|
|
828
|
+
const proceed = await confirm({
|
|
829
|
+
message: "Continue initializing here anyway?",
|
|
830
|
+
default: false
|
|
831
|
+
});
|
|
832
|
+
if (!proceed) {
|
|
833
|
+
console.log(chalk2.dim("Aborted. cd into your project root and try again."));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
console.log();
|
|
837
|
+
}
|
|
838
|
+
const alreadyInitialized = await fs2.pathExists(path2.join(cwd, "AGENTS.md"));
|
|
839
|
+
let isReInit = false;
|
|
840
|
+
let tools;
|
|
841
|
+
if (alreadyInitialized && !options.force) {
|
|
842
|
+
const existing = detectExistingTools(cwd);
|
|
843
|
+
const existingNames = [
|
|
844
|
+
existing.claude ? "Claude Code" : "",
|
|
845
|
+
existing.cursor ? "Cursor" : "",
|
|
846
|
+
existing.opencode ? "OpenCode" : ""
|
|
847
|
+
].filter(Boolean).join(", ");
|
|
848
|
+
if (existing.claude && existing.cursor && existing.opencode) {
|
|
849
|
+
console.log(
|
|
850
|
+
chalk2.yellow("Project already initialized with all tools. Use --force to overwrite.")
|
|
851
|
+
);
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
console.log(
|
|
855
|
+
chalk2.dim(`Project already initialized${existingNames ? ` with ${existingNames}` : ""}.`)
|
|
856
|
+
);
|
|
857
|
+
isReInit = true;
|
|
858
|
+
if (options.claude || options.cursor || options.opencode) {
|
|
859
|
+
tools = {
|
|
860
|
+
claude: !!options.claude,
|
|
861
|
+
cursor: !!options.cursor,
|
|
862
|
+
opencode: !!options.opencode,
|
|
863
|
+
other: false
|
|
864
|
+
};
|
|
865
|
+
} else {
|
|
866
|
+
const addMore = await confirm({
|
|
867
|
+
message: "Add configuration for another tool?",
|
|
868
|
+
default: true
|
|
869
|
+
});
|
|
870
|
+
if (!addMore) {
|
|
871
|
+
process.exit(0);
|
|
872
|
+
}
|
|
873
|
+
tools = await promptToolSelection(existing);
|
|
874
|
+
}
|
|
875
|
+
} else {
|
|
876
|
+
if (options.claude || options.cursor || options.opencode) {
|
|
877
|
+
tools = {
|
|
878
|
+
claude: !!options.claude,
|
|
879
|
+
cursor: !!options.cursor,
|
|
880
|
+
opencode: !!options.opencode,
|
|
881
|
+
other: false
|
|
882
|
+
};
|
|
883
|
+
} else {
|
|
884
|
+
tools = await promptToolSelection();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (!tools.claude && !tools.cursor && !tools.opencode && !tools.other) {
|
|
888
|
+
console.log(chalk2.yellow("No tools selected. Nothing to do."));
|
|
889
|
+
process.exit(0);
|
|
890
|
+
}
|
|
891
|
+
let templates;
|
|
892
|
+
try {
|
|
893
|
+
templates = await fetchTemplates();
|
|
894
|
+
} catch (error) {
|
|
895
|
+
if (error instanceof Error && error.message.includes("expired")) {
|
|
896
|
+
console.error(
|
|
897
|
+
chalk2.red("Session expired. Run `npx @meltstudio/meltctl@latest login` to re-authenticate.")
|
|
898
|
+
);
|
|
899
|
+
} else if (error instanceof Error && error.message.includes("fetch")) {
|
|
900
|
+
console.error(chalk2.red("Could not reach Melt API. Check your connection."));
|
|
901
|
+
} else {
|
|
902
|
+
console.error(
|
|
903
|
+
chalk2.red(
|
|
904
|
+
`Failed to fetch templates: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
905
|
+
)
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
console.log(chalk2.bold("Initializing Melt development tools..."));
|
|
911
|
+
console.log();
|
|
912
|
+
const createdFiles = [];
|
|
913
|
+
const workflows = [
|
|
914
|
+
"setup",
|
|
915
|
+
"plan",
|
|
916
|
+
"validate",
|
|
917
|
+
"review",
|
|
918
|
+
"pr",
|
|
919
|
+
"debug",
|
|
920
|
+
"audit",
|
|
921
|
+
"ux-audit",
|
|
922
|
+
"security-audit",
|
|
923
|
+
"update",
|
|
924
|
+
"help",
|
|
925
|
+
"link"
|
|
926
|
+
];
|
|
927
|
+
if (!isReInit) {
|
|
928
|
+
const agentsMd = templates["agents-md.md"];
|
|
929
|
+
if (agentsMd) {
|
|
930
|
+
await fs2.writeFile(path2.join(cwd, "AGENTS.md"), agentsMd, "utf-8");
|
|
931
|
+
createdFiles.push("AGENTS.md");
|
|
932
|
+
}
|
|
933
|
+
const mcpConfig = templates["mcp-configs/base.json"];
|
|
934
|
+
if (mcpConfig) {
|
|
935
|
+
await mergeMcpConfig(cwd, mcpConfig);
|
|
936
|
+
createdFiles.push(".mcp.json");
|
|
937
|
+
}
|
|
938
|
+
await updateGitignore(cwd);
|
|
939
|
+
}
|
|
940
|
+
if (tools.claude) {
|
|
941
|
+
const claudeSettings = templates["claude-settings.json"];
|
|
942
|
+
if (claudeSettings) {
|
|
943
|
+
await fs2.ensureDir(path2.join(cwd, ".claude"));
|
|
944
|
+
await mergeClaudeSettings(cwd, claudeSettings);
|
|
945
|
+
createdFiles.push(".claude/settings.json");
|
|
946
|
+
}
|
|
947
|
+
for (const name of workflows) {
|
|
948
|
+
const workflowContent = templates[`workflows/${name}.md`];
|
|
949
|
+
if (workflowContent) {
|
|
950
|
+
const skillDir = path2.join(cwd, `.claude/skills/melt-${name}`);
|
|
951
|
+
await fs2.ensureDir(skillDir);
|
|
952
|
+
const skillContent = SKILL_FRONTMATTER[name] + workflowContent;
|
|
953
|
+
await fs2.writeFile(path2.join(skillDir, "SKILL.md"), skillContent, "utf-8");
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
createdFiles.push(
|
|
957
|
+
".claude/skills/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,security-audit,update,help}/SKILL.md"
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (tools.cursor) {
|
|
961
|
+
await fs2.ensureDir(path2.join(cwd, ".cursor/commands"));
|
|
962
|
+
for (const name of workflows) {
|
|
963
|
+
const workflowContent = templates[`workflows/${name}.md`];
|
|
964
|
+
if (workflowContent) {
|
|
965
|
+
await fs2.writeFile(
|
|
966
|
+
path2.join(cwd, `.cursor/commands/melt-${name}.md`),
|
|
967
|
+
workflowContent,
|
|
968
|
+
"utf-8"
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
createdFiles.push(
|
|
973
|
+
".cursor/commands/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,security-audit,update,help}.md"
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
if (tools.opencode) {
|
|
977
|
+
await fs2.ensureDir(path2.join(cwd, ".opencode/commands"));
|
|
978
|
+
for (const name of workflows) {
|
|
979
|
+
const workflowContent = templates[`workflows/${name}.md`];
|
|
980
|
+
if (workflowContent) {
|
|
981
|
+
const commandContent = OPENCODE_COMMAND_FRONTMATTER[name] + workflowContent;
|
|
982
|
+
await fs2.writeFile(
|
|
983
|
+
path2.join(cwd, `.opencode/commands/melt-${name}.md`),
|
|
984
|
+
commandContent,
|
|
985
|
+
"utf-8"
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
createdFiles.push(
|
|
990
|
+
".opencode/commands/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,security-audit,update,help}.md"
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
console.log(chalk2.green("Created files:"));
|
|
994
|
+
for (const file of createdFiles) {
|
|
995
|
+
console.log(chalk2.dim(` ${file}`));
|
|
996
|
+
}
|
|
997
|
+
console.log();
|
|
998
|
+
if (tools.other) {
|
|
999
|
+
console.log(chalk2.cyan("Want support for your tool? Let us know in #dev on Slack"));
|
|
1000
|
+
console.log();
|
|
1001
|
+
}
|
|
1002
|
+
const commandNames = "melt-setup, melt-plan, melt-validate, melt-review, melt-pr, melt-debug, melt-audit, melt-ux-audit, melt-security-audit, melt-update, melt-help";
|
|
1003
|
+
if (tools.claude) {
|
|
1004
|
+
console.log(chalk2.dim(`Available skills: /${commandNames.replace(/, /g, ", /")}`));
|
|
1005
|
+
}
|
|
1006
|
+
if (tools.cursor) {
|
|
1007
|
+
console.log(chalk2.dim(`Available Cursor commands: ${commandNames}`));
|
|
1008
|
+
}
|
|
1009
|
+
if (tools.opencode) {
|
|
1010
|
+
console.log(chalk2.dim(`Available OpenCode commands: /${commandNames.replace(/, /g, ", /")}`));
|
|
1011
|
+
}
|
|
1012
|
+
if (tools.claude || tools.cursor || tools.opencode) {
|
|
1013
|
+
console.log();
|
|
1014
|
+
}
|
|
1015
|
+
if (!isReInit) {
|
|
1016
|
+
console.log(
|
|
1017
|
+
chalk2.bold.cyan(" Next step: run /melt-setup to customize AGENTS.md for your project")
|
|
1018
|
+
);
|
|
1019
|
+
console.log();
|
|
1020
|
+
const toolInstructions = [];
|
|
1021
|
+
if (tools.claude) {
|
|
1022
|
+
toolInstructions.push({
|
|
1023
|
+
name: "Claude Code",
|
|
1024
|
+
steps: [
|
|
1025
|
+
"Type /melt-setup in the chat prompt, or",
|
|
1026
|
+
"Open the skills menu and select melt-setup"
|
|
1027
|
+
]
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
if (tools.cursor) {
|
|
1031
|
+
toolInstructions.push({
|
|
1032
|
+
name: "Cursor",
|
|
1033
|
+
steps: ["Open the command menu (Cmd+K) and search for melt-setup"]
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (tools.opencode) {
|
|
1037
|
+
toolInstructions.push({
|
|
1038
|
+
name: "OpenCode",
|
|
1039
|
+
steps: ["Type /melt-setup in the chat prompt"]
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (toolInstructions.length === 1 && toolInstructions[0]) {
|
|
1043
|
+
console.log(chalk2.dim(` In ${toolInstructions[0].name}:`));
|
|
1044
|
+
for (const step of toolInstructions[0].steps) {
|
|
1045
|
+
console.log(chalk2.dim(` - ${step}`));
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
for (const tool of toolInstructions) {
|
|
1049
|
+
console.log(chalk2.dim(` ${tool.name}:`));
|
|
1050
|
+
for (const step of tool.steps) {
|
|
1051
|
+
console.log(chalk2.dim(` - ${step}`));
|
|
1052
|
+
}
|
|
1053
|
+
console.log();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
console.log();
|
|
1057
|
+
console.log(chalk2.dim(" The setup skill will analyze your project and fill in AGENTS.md."));
|
|
1058
|
+
console.log(
|
|
1059
|
+
chalk2.dim(" Then run /melt-link to connect your ticket tracker and browser testing tools.")
|
|
1060
|
+
);
|
|
1061
|
+
console.log();
|
|
1062
|
+
}
|
|
1063
|
+
console.log(chalk2.green("Done!"));
|
|
1064
|
+
}
|
|
1065
|
+
async function mergeMcpConfig(cwd, templateContent) {
|
|
1066
|
+
const mcpPath = path2.join(cwd, ".mcp.json");
|
|
1067
|
+
const templateConfig = JSON.parse(templateContent);
|
|
1068
|
+
if (await fs2.pathExists(mcpPath)) {
|
|
1069
|
+
const existingContent = await fs2.readFile(mcpPath, "utf-8");
|
|
1070
|
+
const existingConfig = JSON.parse(existingContent);
|
|
1071
|
+
existingConfig.mcpServers = {
|
|
1072
|
+
...existingConfig.mcpServers,
|
|
1073
|
+
...templateConfig.mcpServers
|
|
1074
|
+
};
|
|
1075
|
+
await fs2.writeFile(mcpPath, JSON.stringify(existingConfig, null, 2) + "\n", "utf-8");
|
|
1076
|
+
} else {
|
|
1077
|
+
await fs2.writeFile(mcpPath, templateContent, "utf-8");
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async function mergeClaudeSettings(cwd, templateContent) {
|
|
1081
|
+
const settingsPath = path2.join(cwd, ".claude/settings.json");
|
|
1082
|
+
const templateConfig = JSON.parse(templateContent);
|
|
1083
|
+
if (await fs2.pathExists(settingsPath)) {
|
|
1084
|
+
const existingContent = await fs2.readFile(settingsPath, "utf-8");
|
|
1085
|
+
const existingConfig = JSON.parse(existingContent);
|
|
1086
|
+
const existingAllow = existingConfig.permissions?.allow ?? [];
|
|
1087
|
+
const existingDeny = existingConfig.permissions?.deny ?? [];
|
|
1088
|
+
const templateAllow = templateConfig.permissions?.allow ?? [];
|
|
1089
|
+
const templateDeny = templateConfig.permissions?.deny ?? [];
|
|
1090
|
+
existingConfig.permissions = {
|
|
1091
|
+
...existingConfig.permissions,
|
|
1092
|
+
allow: [.../* @__PURE__ */ new Set([...existingAllow, ...templateAllow])],
|
|
1093
|
+
deny: [.../* @__PURE__ */ new Set([...existingDeny, ...templateDeny])]
|
|
1094
|
+
};
|
|
1095
|
+
await fs2.writeFile(settingsPath, JSON.stringify(existingConfig, null, 2) + "\n", "utf-8");
|
|
1096
|
+
} else {
|
|
1097
|
+
await fs2.writeFile(settingsPath, templateContent, "utf-8");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async function updateGitignore(cwd) {
|
|
1101
|
+
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
1102
|
+
let content = "";
|
|
1103
|
+
if (await fs2.pathExists(gitignorePath)) {
|
|
1104
|
+
content = await fs2.readFile(gitignorePath, "utf-8");
|
|
1105
|
+
}
|
|
1106
|
+
const missingEntries = GITIGNORE_ENTRIES.filter((entry) => !content.includes(entry));
|
|
1107
|
+
if (missingEntries.length > 0) {
|
|
1108
|
+
const suffix = content.endsWith("\n") || content === "" ? "" : "\n";
|
|
1109
|
+
const section = missingEntries.length > 0 ? `${suffix}
|
|
1110
|
+
# Melt - local settings
|
|
1111
|
+
${missingEntries.join("\n")}
|
|
1112
|
+
` : "";
|
|
1113
|
+
await fs2.writeFile(gitignorePath, content + section, "utf-8");
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/commands/templates.ts
|
|
1118
|
+
import chalk3 from "chalk";
|
|
1119
|
+
import fs3 from "fs-extra";
|
|
1120
|
+
import path3 from "path";
|
|
1121
|
+
import os2 from "os";
|
|
1122
|
+
async function templatesCommand() {
|
|
1123
|
+
if (!await isAuthenticated()) {
|
|
1124
|
+
console.error(chalk3.red("Not authenticated. Run `npx @meltstudio/meltctl@latest login` first."));
|
|
1125
|
+
process.exit(1);
|
|
1126
|
+
}
|
|
1127
|
+
let templates;
|
|
1128
|
+
try {
|
|
1129
|
+
templates = await fetchTemplates();
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
if (error instanceof Error && error.message.includes("expired")) {
|
|
1132
|
+
console.error(
|
|
1133
|
+
chalk3.red("Session expired. Run `npx @meltstudio/meltctl@latest login` to re-authenticate.")
|
|
1134
|
+
);
|
|
1135
|
+
} else if (error instanceof Error && error.message.includes("fetch")) {
|
|
1136
|
+
console.error(chalk3.red("Could not reach Melt API. Check your connection."));
|
|
1137
|
+
} else {
|
|
1138
|
+
console.error(
|
|
1139
|
+
chalk3.red(
|
|
1140
|
+
`Failed to fetch templates: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1141
|
+
)
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
const tmpDir = path3.join(os2.tmpdir(), `melt-templates-${Date.now()}`);
|
|
1147
|
+
await fs3.ensureDir(tmpDir);
|
|
1148
|
+
for (const [filePath, content] of Object.entries(templates)) {
|
|
1149
|
+
const fullPath = path3.join(tmpDir, filePath);
|
|
1150
|
+
await fs3.ensureDir(path3.dirname(fullPath));
|
|
1151
|
+
await fs3.writeFile(fullPath, content, "utf-8");
|
|
1152
|
+
}
|
|
1153
|
+
console.log(tmpDir);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/commands/login.ts
|
|
1157
|
+
import chalk5 from "chalk";
|
|
1158
|
+
import http from "http";
|
|
1159
|
+
import { URL } from "url";
|
|
1160
|
+
import { exec } from "child_process";
|
|
1161
|
+
|
|
1162
|
+
// src/utils/banner.ts
|
|
1163
|
+
init_version();
|
|
1164
|
+
import chalk4 from "chalk";
|
|
1165
|
+
import gradient from "gradient-string";
|
|
1166
|
+
var meltGradient = gradient(["#FFC107", "#E91E63", "#00BCD4", "#FF5722"]);
|
|
1167
|
+
var LOGO = `
|
|
1168
|
+
\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557
|
|
1169
|
+
\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551
|
|
1170
|
+
\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
|
|
1171
|
+
\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
|
|
1172
|
+
\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
1173
|
+
\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D`;
|
|
1174
|
+
function printBanner() {
|
|
1175
|
+
console.log(meltGradient(LOGO));
|
|
1176
|
+
console.log();
|
|
1177
|
+
console.log(
|
|
1178
|
+
` ${chalk4.dim("v" + CLI_VERSION)} ${chalk4.dim("\xB7")} ${chalk4.dim("AI-first development tools for teams")}`
|
|
1179
|
+
);
|
|
1180
|
+
console.log();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/commands/login.ts
|
|
1184
|
+
var LOGIN_TIMEOUT_MS = 6e4;
|
|
1185
|
+
function openBrowser(url) {
|
|
1186
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1187
|
+
exec(`${command} "${url}"`);
|
|
1188
|
+
}
|
|
1189
|
+
function findFreePort() {
|
|
1190
|
+
return new Promise((resolve, reject) => {
|
|
1191
|
+
const server = http.createServer();
|
|
1192
|
+
server.listen(0, () => {
|
|
1193
|
+
const address = server.address();
|
|
1194
|
+
if (address && typeof address === "object") {
|
|
1195
|
+
const port = address.port;
|
|
1196
|
+
server.close(() => resolve(port));
|
|
1197
|
+
} else {
|
|
1198
|
+
reject(new Error("Could not find free port"));
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
async function loginCommand() {
|
|
1204
|
+
printBanner();
|
|
1205
|
+
console.log(chalk5.bold(" Logging in to Melt..."));
|
|
1206
|
+
const port = await findFreePort();
|
|
1207
|
+
const redirectUri = `http://localhost:${port.toString()}`;
|
|
1208
|
+
const authCode = await new Promise((resolve, reject) => {
|
|
1209
|
+
const timeout = setTimeout(() => {
|
|
1210
|
+
server.close();
|
|
1211
|
+
reject(new Error("Authentication timed out. Please try again."));
|
|
1212
|
+
}, LOGIN_TIMEOUT_MS);
|
|
1213
|
+
const server = http.createServer((req, res) => {
|
|
1214
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port.toString()}`);
|
|
1215
|
+
const code = url.searchParams.get("code");
|
|
1216
|
+
const error = url.searchParams.get("error");
|
|
1217
|
+
if (error) {
|
|
1218
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1219
|
+
res.end(
|
|
1220
|
+
"<html><body><h2>Authentication failed</h2><p>You can close this tab.</p></body></html>"
|
|
1221
|
+
);
|
|
1222
|
+
clearTimeout(timeout);
|
|
1223
|
+
server.close();
|
|
1224
|
+
reject(new Error(`Authentication denied: ${error}`));
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (!code) {
|
|
1228
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1229
|
+
res.end("<html><body><h2>Missing authorization code</h2></body></html>");
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1233
|
+
res.end(
|
|
1234
|
+
"<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>"
|
|
1235
|
+
);
|
|
1236
|
+
clearTimeout(timeout);
|
|
1237
|
+
server.close();
|
|
1238
|
+
resolve(code);
|
|
1239
|
+
});
|
|
1240
|
+
server.listen(port, () => {
|
|
1241
|
+
const authUrl = `${API_BASE}/auth/google?redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
1242
|
+
console.log();
|
|
1243
|
+
console.log(chalk5.bold(" A browser window will open shortly."));
|
|
1244
|
+
console.log(chalk5.dim(" Please log in with your @meltstudio.co Google Workspace account."));
|
|
1245
|
+
console.log();
|
|
1246
|
+
setTimeout(() => {
|
|
1247
|
+
openBrowser(authUrl);
|
|
1248
|
+
console.log(chalk5.dim(` If the browser didn't open, visit: ${authUrl}`));
|
|
1249
|
+
}, 2e3);
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
console.log(chalk5.dim("Exchanging authorization code..."));
|
|
1253
|
+
const client = createMeltClient({ baseUrl: API_BASE, token: "" });
|
|
1254
|
+
try {
|
|
1255
|
+
const data = await client.auth.exchangeToken(authCode, redirectUri);
|
|
1256
|
+
await storeAuth({
|
|
1257
|
+
token: data.token,
|
|
1258
|
+
email: data.email,
|
|
1259
|
+
expiresAt: data.expiresAt
|
|
1260
|
+
});
|
|
1261
|
+
console.log();
|
|
1262
|
+
console.log(chalk5.green(`Logged in as ${data.email}`));
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
console.error(
|
|
1265
|
+
chalk5.red(
|
|
1266
|
+
`Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1267
|
+
)
|
|
1268
|
+
);
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/commands/logout.ts
|
|
1274
|
+
import chalk6 from "chalk";
|
|
1275
|
+
async function logoutCommand() {
|
|
1276
|
+
const auth = await getStoredAuth();
|
|
1277
|
+
await clearAuth();
|
|
1278
|
+
if (auth) {
|
|
1279
|
+
console.log(chalk6.green(`Logged out (was ${auth.email})`));
|
|
1280
|
+
} else {
|
|
1281
|
+
console.log(chalk6.dim("No active session found."));
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// src/index.ts
|
|
1286
|
+
init_version_check();
|
|
1287
|
+
|
|
1288
|
+
// src/commands/version.ts
|
|
1289
|
+
init_version_check();
|
|
1290
|
+
import chalk9 from "chalk";
|
|
1291
|
+
|
|
1292
|
+
// src/utils/package-manager.ts
|
|
1293
|
+
import { execSync as execSync3 } from "child_process";
|
|
1294
|
+
function detectPackageManager2() {
|
|
1295
|
+
try {
|
|
1296
|
+
execSync3("npm list -g @meltstudio/meltctl", {
|
|
1297
|
+
encoding: "utf-8",
|
|
1298
|
+
stdio: "pipe"
|
|
1299
|
+
});
|
|
1300
|
+
return {
|
|
1301
|
+
type: "npm",
|
|
1302
|
+
updateCommand: "npm install -g @meltstudio/meltctl@latest"
|
|
1303
|
+
};
|
|
1304
|
+
} catch {
|
|
1305
|
+
}
|
|
1306
|
+
try {
|
|
1307
|
+
execSync3("yarn global list @meltstudio/meltctl", {
|
|
1308
|
+
encoding: "utf-8",
|
|
1309
|
+
stdio: "pipe"
|
|
1310
|
+
});
|
|
1311
|
+
return {
|
|
1312
|
+
type: "yarn",
|
|
1313
|
+
updateCommand: "yarn global add @meltstudio/meltctl@latest"
|
|
1314
|
+
};
|
|
1315
|
+
} catch {
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
type: "unknown",
|
|
1319
|
+
updateCommand: "npm install -g @meltstudio/meltctl@latest"
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
function getUpdateInstructions() {
|
|
1323
|
+
const { type, updateCommand: updateCommand2 } = detectPackageManager2();
|
|
1324
|
+
if (type === "npm") {
|
|
1325
|
+
return [updateCommand2, "", "Or with yarn:", " yarn global add @meltstudio/meltctl@latest"];
|
|
1326
|
+
} else if (type === "yarn") {
|
|
1327
|
+
return [updateCommand2, "", "Or with npm:", " npm install -g @meltstudio/meltctl@latest"];
|
|
1328
|
+
} else {
|
|
1329
|
+
return [
|
|
1330
|
+
"npm install -g @meltstudio/meltctl@latest",
|
|
1331
|
+
"",
|
|
1332
|
+
"Or with yarn:",
|
|
1333
|
+
" yarn global add @meltstudio/meltctl@latest"
|
|
1334
|
+
];
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// src/commands/version.ts
|
|
1339
|
+
async function versionCheckCommand() {
|
|
1340
|
+
try {
|
|
1341
|
+
const currentVersion = await getCurrentCliVersion();
|
|
1342
|
+
const latestVersion = await getLatestCliVersion();
|
|
1343
|
+
if (!latestVersion) {
|
|
1344
|
+
console.log(chalk9.yellow("\u26A0\uFE0F Unable to check for updates (network error)"));
|
|
1345
|
+
console.log(chalk9.gray(`Current version: ${currentVersion}`));
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (compareVersions(currentVersion, latestVersion)) {
|
|
1349
|
+
console.log(chalk9.yellow(`\u26A0 Update available: ${currentVersion} \u2192 ${latestVersion}`));
|
|
1350
|
+
console.log();
|
|
1351
|
+
console.log(chalk9.white("To update, run:"));
|
|
1352
|
+
const instructions = getUpdateInstructions();
|
|
1353
|
+
instructions.forEach((instruction, index) => {
|
|
1354
|
+
if (index === 0) {
|
|
1355
|
+
console.log(chalk9.cyan(` ${instruction}`));
|
|
1356
|
+
} else if (instruction === "") {
|
|
1357
|
+
console.log();
|
|
1358
|
+
} else if (instruction.startsWith("Or with")) {
|
|
1359
|
+
console.log(chalk9.gray(instruction));
|
|
1360
|
+
} else {
|
|
1361
|
+
console.log(chalk9.cyan(instruction));
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
} else {
|
|
1365
|
+
console.log(chalk9.green(`\u2713 meltctl ${currentVersion} is up to date`));
|
|
1366
|
+
}
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
console.error(
|
|
1369
|
+
chalk9.red("Failed to check for updates:"),
|
|
1370
|
+
error instanceof Error ? error.message : String(error)
|
|
1371
|
+
);
|
|
1372
|
+
process.exit(1);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/commands/standup.ts
|
|
1377
|
+
import chalk10 from "chalk";
|
|
1378
|
+
import { input, editor } from "@inquirer/prompts";
|
|
1379
|
+
var EDITOR_HINT = chalk10.dim("(type \\e to open your editor)");
|
|
1380
|
+
async function promptField(message, required) {
|
|
1381
|
+
const value = await input({ message: `${message} ${EDITOR_HINT}` });
|
|
1382
|
+
if (value === "\\e") {
|
|
1383
|
+
try {
|
|
1384
|
+
return await editor({ message });
|
|
1385
|
+
} catch {
|
|
1386
|
+
console.log(chalk10.yellow("Editor failed to open. Falling back to inline input."));
|
|
1387
|
+
return promptField(message, required);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (required && !value.trim()) {
|
|
1391
|
+
console.log(chalk10.yellow("This field is required."));
|
|
1392
|
+
return promptField(message, required);
|
|
1393
|
+
}
|
|
1394
|
+
return value;
|
|
1395
|
+
}
|
|
1396
|
+
async function standupCommand(options) {
|
|
1397
|
+
if (!await isAuthenticated()) {
|
|
1398
|
+
console.error(chalk10.red("Not authenticated. Run `npx @meltstudio/meltctl@latest login` first."));
|
|
1399
|
+
process.exit(1);
|
|
1400
|
+
}
|
|
1401
|
+
const client = await getClient();
|
|
1402
|
+
try {
|
|
1403
|
+
const existing = await client.standups.getStatus();
|
|
1404
|
+
if (existing) {
|
|
1405
|
+
console.log(chalk10.yellow("\nYou already submitted a standup today:\n"));
|
|
1406
|
+
console.log(chalk10.bold("Yesterday:"), existing.yesterday);
|
|
1407
|
+
console.log(chalk10.bold("Today:"), existing.today);
|
|
1408
|
+
if (existing.blockers) {
|
|
1409
|
+
console.log(chalk10.bold("Blockers:"), existing.blockers);
|
|
1410
|
+
}
|
|
1411
|
+
console.log(chalk10.dim("\nTo edit your standup, use the Melt Connect frontend."));
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
} catch {
|
|
1415
|
+
}
|
|
1416
|
+
let yesterday;
|
|
1417
|
+
let today;
|
|
1418
|
+
let blockers;
|
|
1419
|
+
if (options.yesterday && options.today) {
|
|
1420
|
+
yesterday = options.yesterday;
|
|
1421
|
+
today = options.today;
|
|
1422
|
+
blockers = options.blockers ?? "";
|
|
1423
|
+
} else {
|
|
1424
|
+
console.log(chalk10.bold.cyan("\n Daily Standup Report\n"));
|
|
1425
|
+
console.log(chalk10.dim(" Tip: Mention tickets, PRs, or features by name.\n"));
|
|
1426
|
+
yesterday = await promptField("What did you work on yesterday?", true);
|
|
1427
|
+
today = await promptField("What are you going to work on today?", true);
|
|
1428
|
+
blockers = await promptField("Any blockers? (leave empty if none)", false);
|
|
1429
|
+
}
|
|
1430
|
+
try {
|
|
1431
|
+
await client.standups.submit({
|
|
1432
|
+
yesterday,
|
|
1433
|
+
today,
|
|
1434
|
+
blockers: blockers || void 0
|
|
1435
|
+
});
|
|
1436
|
+
console.log(chalk10.green("\n \u2713 Standup submitted!\n"));
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
console.error(
|
|
1439
|
+
chalk10.red(
|
|
1440
|
+
`
|
|
1441
|
+
Failed to submit standup: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1442
|
+
)
|
|
1443
|
+
);
|
|
1444
|
+
process.exit(1);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/commands/feedback.ts
|
|
1449
|
+
import chalk11 from "chalk";
|
|
1450
|
+
import { input as input2, select, editor as editor2 } from "@inquirer/prompts";
|
|
1451
|
+
var EDITOR_HINT2 = chalk11.dim("(type \\\\e to open your editor)");
|
|
1452
|
+
async function promptDescription() {
|
|
1453
|
+
const value = await input2({
|
|
1454
|
+
message: `Write your feedback (50\u2013500 chars): ${EDITOR_HINT2}`
|
|
1455
|
+
});
|
|
1456
|
+
if (value === "\\e") {
|
|
1457
|
+
try {
|
|
1458
|
+
const editorValue = await editor2({ message: "Write your feedback:" });
|
|
1459
|
+
return validateDescription(editorValue);
|
|
1460
|
+
} catch {
|
|
1461
|
+
console.log(chalk11.yellow("Editor failed to open. Falling back to inline input."));
|
|
1462
|
+
return promptDescription();
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return validateDescription(value);
|
|
1466
|
+
}
|
|
1467
|
+
function validateDescription(value) {
|
|
1468
|
+
const trimmed = value.trim();
|
|
1469
|
+
if (trimmed.length < 50) {
|
|
1470
|
+
console.log(chalk11.yellow(`Too short (${trimmed.length}/50 min). Please write more.`));
|
|
1471
|
+
return promptDescription();
|
|
1472
|
+
}
|
|
1473
|
+
if (trimmed.length > 500) {
|
|
1474
|
+
console.log(chalk11.yellow(`Too long (${trimmed.length}/500 max). Please shorten it.`));
|
|
1475
|
+
return promptDescription();
|
|
1476
|
+
}
|
|
1477
|
+
return trimmed;
|
|
1478
|
+
}
|
|
1479
|
+
async function feedbackCommand(options) {
|
|
1480
|
+
if (!await isAuthenticated()) {
|
|
1481
|
+
console.error(chalk11.red("Not authenticated. Run `npx @meltstudio/meltctl@latest login` first."));
|
|
1482
|
+
process.exit(1);
|
|
1483
|
+
}
|
|
1484
|
+
const client = await getClient();
|
|
1485
|
+
let toUserId;
|
|
1486
|
+
let coins;
|
|
1487
|
+
let description;
|
|
1488
|
+
if (options.to && options.coins && options.description) {
|
|
1489
|
+
toUserId = parseInt(options.to, 10);
|
|
1490
|
+
coins = parseInt(options.coins, 10);
|
|
1491
|
+
description = options.description;
|
|
1492
|
+
} else {
|
|
1493
|
+
console.log(chalk11.bold.cyan("\n Send Feedback\n"));
|
|
1494
|
+
let recipients;
|
|
1495
|
+
try {
|
|
1496
|
+
recipients = await client.feedback.getRecipients();
|
|
1497
|
+
} catch {
|
|
1498
|
+
console.error(chalk11.red("Failed to connect to the server."));
|
|
1499
|
+
process.exit(1);
|
|
1500
|
+
}
|
|
1501
|
+
if (recipients.length === 0) {
|
|
1502
|
+
console.log(chalk11.yellow("No recipients available."));
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const selectedRecipient = await select({
|
|
1506
|
+
message: "Who would you like to recognize?",
|
|
1507
|
+
choices: recipients.map((r) => ({
|
|
1508
|
+
name: `${r.firstName} ${r.lastName} (${r.email})`,
|
|
1509
|
+
value: r.id
|
|
1510
|
+
}))
|
|
1511
|
+
});
|
|
1512
|
+
toUserId = selectedRecipient;
|
|
1513
|
+
coins = await select({
|
|
1514
|
+
message: "How many coins?",
|
|
1515
|
+
choices: [
|
|
1516
|
+
{ name: "1 coin", value: 1 },
|
|
1517
|
+
{ name: "2 coins", value: 2 },
|
|
1518
|
+
{ name: "3 coins", value: 3 }
|
|
1519
|
+
]
|
|
1520
|
+
});
|
|
1521
|
+
description = await promptDescription();
|
|
1522
|
+
}
|
|
1523
|
+
try {
|
|
1524
|
+
await client.feedback.submit({ toUserId, coins, description });
|
|
1525
|
+
console.log(
|
|
1526
|
+
chalk11.green(`
|
|
1527
|
+
\u2713 Feedback sent! You gave ${coins} coin${coins > 1 ? "s" : ""}.
|
|
1528
|
+
`)
|
|
1529
|
+
);
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
console.error(
|
|
1532
|
+
chalk11.red(
|
|
1533
|
+
`
|
|
1534
|
+
Failed to send feedback: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1535
|
+
)
|
|
1536
|
+
);
|
|
1537
|
+
process.exit(1);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/commands/coins.ts
|
|
1542
|
+
import chalk12 from "chalk";
|
|
1543
|
+
async function coinsCommand(options) {
|
|
1544
|
+
if (!await isAuthenticated()) {
|
|
1545
|
+
console.error(chalk12.red("Not authenticated. Run `npx @meltstudio/meltctl@latest login` first."));
|
|
1546
|
+
process.exit(1);
|
|
1547
|
+
}
|
|
1548
|
+
if (options.leaderboard) {
|
|
1549
|
+
await showLeaderboard();
|
|
1550
|
+
} else {
|
|
1551
|
+
await showBalance();
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async function showBalance() {
|
|
1555
|
+
const client = await getClient();
|
|
1556
|
+
try {
|
|
1557
|
+
const data = await client.coins.getBalance();
|
|
1558
|
+
console.log(chalk12.bold.cyan("\n Your Coins (last 28 days)"));
|
|
1559
|
+
console.log(` ${chalk12.bold(String(data.coins))} coin${data.coins !== 1 ? "s" : ""} received
|
|
173
1560
|
`);
|
|
174
|
-
|
|
175
|
-
.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
console.error(
|
|
1563
|
+
chalk12.red(
|
|
1564
|
+
`Failed to fetch coins: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1565
|
+
)
|
|
1566
|
+
);
|
|
1567
|
+
process.exit(1);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
async function showLeaderboard() {
|
|
1571
|
+
const client = await getClient();
|
|
1572
|
+
try {
|
|
1573
|
+
const entries = await client.coins.getLeaderboard();
|
|
1574
|
+
if (entries.length === 0) {
|
|
1575
|
+
console.log(chalk12.yellow("\n No coins have been sent in the last 28 days.\n"));
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
console.log(chalk12.bold.cyan("\n Leaderboard (last 28 days)\n"));
|
|
1579
|
+
const maxNameLen = Math.max(...entries.map((e) => e.name.length), 4);
|
|
1580
|
+
console.log(chalk12.dim(` ${"#".padEnd(4)} ${"Name".padEnd(maxNameLen)} Coins`));
|
|
1581
|
+
entries.forEach((entry, i) => {
|
|
1582
|
+
const rank = String(i + 1).padEnd(4);
|
|
1583
|
+
const name = entry.name.padEnd(maxNameLen);
|
|
1584
|
+
const coins = String(entry.coins);
|
|
1585
|
+
console.log(` ${rank} ${name} ${coins}`);
|
|
1586
|
+
});
|
|
1587
|
+
console.log();
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
console.error(
|
|
1590
|
+
chalk12.red(
|
|
1591
|
+
`Failed to fetch leaderboard: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1592
|
+
)
|
|
1593
|
+
);
|
|
1594
|
+
process.exit(1);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/commands/audit.ts
|
|
1599
|
+
import chalk13 from "chalk";
|
|
1600
|
+
import fs5 from "fs-extra";
|
|
1601
|
+
import path6 from "path";
|
|
1602
|
+
|
|
1603
|
+
// src/utils/git.ts
|
|
1604
|
+
import fs4 from "fs-extra";
|
|
1605
|
+
import path5 from "path";
|
|
1606
|
+
import { execSync as execSync4 } from "child_process";
|
|
1607
|
+
function getGitBranch() {
|
|
1608
|
+
try {
|
|
1609
|
+
return execSync4("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
|
|
1610
|
+
} catch {
|
|
1611
|
+
return "unknown";
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function getGitCommit() {
|
|
1615
|
+
try {
|
|
1616
|
+
return execSync4("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
1617
|
+
} catch {
|
|
1618
|
+
return "unknown";
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function getGitRepository() {
|
|
1622
|
+
try {
|
|
1623
|
+
const url = execSync4("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
1624
|
+
const match = url.match(/[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
1625
|
+
const slug = match ? match[1] : url;
|
|
1626
|
+
return { slug, url };
|
|
1627
|
+
} catch {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
function getProjectName() {
|
|
1632
|
+
const cwd = process.cwd();
|
|
1633
|
+
try {
|
|
1634
|
+
const pkgPath = path5.join(cwd, "package.json");
|
|
1635
|
+
if (fs4.pathExistsSync(pkgPath)) {
|
|
1636
|
+
const pkg = fs4.readJsonSync(pkgPath);
|
|
1637
|
+
if (pkg.name) {
|
|
1638
|
+
return pkg.name;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
return path5.basename(cwd);
|
|
1644
|
+
}
|
|
1645
|
+
function extractTicketId(branch) {
|
|
1646
|
+
const match = branch.match(/([A-Z]+-\d+)/i);
|
|
1647
|
+
return match ? match[1] : null;
|
|
1648
|
+
}
|
|
1649
|
+
async function findMdFiles(dir) {
|
|
1650
|
+
if (!await fs4.pathExists(dir)) {
|
|
1651
|
+
return [];
|
|
1652
|
+
}
|
|
1653
|
+
const results = [];
|
|
1654
|
+
async function walk(current) {
|
|
1655
|
+
const entries = await fs4.readdir(current, { withFileTypes: true });
|
|
1656
|
+
for (const entry of entries) {
|
|
1657
|
+
const fullPath = path5.join(current, entry.name);
|
|
1658
|
+
if (entry.isDirectory()) {
|
|
1659
|
+
await walk(fullPath);
|
|
1660
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1661
|
+
const stat = await fs4.stat(fullPath);
|
|
1662
|
+
results.push({ path: fullPath, mtime: stat.mtimeMs });
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
await walk(dir);
|
|
1667
|
+
results.sort((a, b) => b.mtime - a.mtime);
|
|
1668
|
+
return results.map((r) => r.path);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// src/commands/audit.ts
|
|
1672
|
+
function detectAuditType(filename) {
|
|
1673
|
+
const lower = filename.toLowerCase();
|
|
1674
|
+
if (lower.includes("security-audit")) return "security-audit";
|
|
1675
|
+
if (lower.includes("ux-audit")) return "ux-audit";
|
|
1676
|
+
return "audit";
|
|
1677
|
+
}
|
|
1678
|
+
async function autoDetectAuditFile() {
|
|
1679
|
+
const cwd = process.cwd();
|
|
1680
|
+
const auditsDir = path6.join(cwd, ".audits");
|
|
1681
|
+
const auditFiles = await findMdFiles(auditsDir);
|
|
1682
|
+
if (auditFiles.length > 0) {
|
|
1683
|
+
return auditFiles[0] ?? null;
|
|
1684
|
+
}
|
|
1685
|
+
const candidates = ["AUDIT.md", "UX-AUDIT.md", "SECURITY-AUDIT.md"];
|
|
1686
|
+
for (const name of candidates) {
|
|
1687
|
+
const filePath = path6.join(cwd, name);
|
|
1688
|
+
if (await fs5.pathExists(filePath)) {
|
|
1689
|
+
return filePath;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
async function auditSubmitCommand(file) {
|
|
1695
|
+
const client = await getClient();
|
|
1696
|
+
let filePath;
|
|
1697
|
+
if (file) {
|
|
1698
|
+
filePath = path6.resolve(file);
|
|
1699
|
+
} else {
|
|
1700
|
+
const detected = await autoDetectAuditFile();
|
|
1701
|
+
if (!detected) {
|
|
1702
|
+
console.error(
|
|
1703
|
+
chalk13.red(
|
|
1704
|
+
"No audit file found. Provide a file path or create an audit in the .audits/ directory."
|
|
1705
|
+
)
|
|
1706
|
+
);
|
|
1707
|
+
process.exit(1);
|
|
1708
|
+
}
|
|
1709
|
+
filePath = detected;
|
|
1710
|
+
console.log(chalk13.dim(`Auto-detected audit file: ${path6.relative(process.cwd(), filePath)}`));
|
|
1711
|
+
}
|
|
1712
|
+
if (!await fs5.pathExists(filePath)) {
|
|
1713
|
+
console.error(chalk13.red(`File not found: ${filePath}`));
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
1717
|
+
const filename = path6.basename(filePath);
|
|
1718
|
+
const auditType = detectAuditType(filename);
|
|
1719
|
+
const project2 = getProjectName();
|
|
1720
|
+
const branch = getGitBranch();
|
|
1721
|
+
const commit = getGitCommit();
|
|
1722
|
+
const repo = getGitRepository();
|
|
1723
|
+
try {
|
|
1724
|
+
const result = await client.audits.submit({
|
|
1725
|
+
type: auditType,
|
|
1726
|
+
project: project2,
|
|
1727
|
+
repository: repo?.slug ?? null,
|
|
1728
|
+
repositoryUrl: repo?.url ?? null,
|
|
1729
|
+
branch,
|
|
1730
|
+
commit,
|
|
1731
|
+
content,
|
|
1732
|
+
metadata: { filename }
|
|
1733
|
+
});
|
|
1734
|
+
console.log(chalk13.green(`
|
|
1735
|
+
\u2713 Audit submitted! ID: ${result.id}
|
|
1736
|
+
`));
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
console.error(
|
|
1739
|
+
chalk13.red(
|
|
1740
|
+
`
|
|
1741
|
+
Failed to submit audit: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1742
|
+
)
|
|
1743
|
+
);
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
async function auditListCommand(options) {
|
|
1748
|
+
const client = await getClient();
|
|
1749
|
+
try {
|
|
1750
|
+
const body = await client.audits.list({
|
|
1751
|
+
type: options.type,
|
|
1752
|
+
repository: options.repository,
|
|
1753
|
+
latest: options.latest,
|
|
1754
|
+
limit: options.limit ? parseInt(options.limit, 10) : void 0
|
|
1755
|
+
});
|
|
1756
|
+
if (body.audits.length === 0) {
|
|
1757
|
+
console.log(chalk13.dim("\n No audits found.\n"));
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const typeLabels = {
|
|
1761
|
+
audit: "Tech Audit",
|
|
1762
|
+
"ux-audit": "UX Audit",
|
|
1763
|
+
"security-audit": "Security"
|
|
1764
|
+
};
|
|
1765
|
+
if (options.latest) {
|
|
1766
|
+
console.log(chalk13.bold(`
|
|
1767
|
+
Latest Audits (${body.count}):
|
|
1768
|
+
`));
|
|
1769
|
+
console.log(
|
|
1770
|
+
chalk13.dim(
|
|
1771
|
+
` ${"ID".padEnd(10)} ${"TYPE".padEnd(12)} ${"REPOSITORY".padEnd(40)} ${"AGE".padEnd(10)} ${"AUTHOR".padEnd(30)} DATE`
|
|
1772
|
+
)
|
|
1773
|
+
);
|
|
1774
|
+
console.log();
|
|
1775
|
+
for (const r of body.audits) {
|
|
1776
|
+
printLatestAuditRow(r, typeLabels);
|
|
1777
|
+
}
|
|
1778
|
+
} else {
|
|
1779
|
+
console.log(chalk13.bold(`
|
|
1780
|
+
Audits (${body.count}):
|
|
1781
|
+
`));
|
|
1782
|
+
const hdr = ` ${"ID".padEnd(10)} ${"TYPE".padEnd(12)} ${"REPOSITORY".padEnd(40)} ${"AUTHOR".padEnd(30)} DATE`;
|
|
1783
|
+
console.log(chalk13.dim(hdr));
|
|
1784
|
+
console.log();
|
|
1785
|
+
for (const r of body.audits) {
|
|
1786
|
+
printAuditRow(r, typeLabels);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
console.log();
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
console.error(
|
|
1792
|
+
chalk13.red(
|
|
1793
|
+
`Failed to list audits: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1794
|
+
)
|
|
1795
|
+
);
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function printLatestAuditRow(r, typeLabels) {
|
|
1800
|
+
const createdAt = new Date(r.created_at ?? r.createdAt);
|
|
1801
|
+
const daysAgo = Math.floor((Date.now() - createdAt.getTime()) / (1e3 * 60 * 60 * 24));
|
|
1802
|
+
const date = createdAt.toLocaleDateString("en-US", {
|
|
1803
|
+
month: "short",
|
|
1804
|
+
day: "numeric",
|
|
1805
|
+
year: "numeric"
|
|
1806
|
+
});
|
|
1807
|
+
const repo = r.repository ?? r.project;
|
|
1808
|
+
const label = typeLabels[r.type] ?? r.type;
|
|
1809
|
+
const typeColor = r.type === "ux-audit" ? chalk13.yellow : r.type === "security-audit" ? chalk13.red : chalk13.magenta;
|
|
1810
|
+
const ageText = daysAgo === 0 ? "today" : `${daysAgo}d ago`;
|
|
1811
|
+
const isSecurityAudit = r.type === "security-audit";
|
|
1812
|
+
const ageColor = isSecurityAudit ? daysAgo <= 30 ? chalk13.green : daysAgo <= 90 ? chalk13.yellow : chalk13.red : daysAgo <= 7 ? chalk13.green : daysAgo <= 30 ? chalk13.yellow : chalk13.red;
|
|
1813
|
+
console.log(
|
|
1814
|
+
` ${chalk13.dim(r.id.slice(0, 8).padEnd(10))} ${typeColor(label.padEnd(12))} ${chalk13.white(repo.padEnd(40))} ${ageColor(ageText.padEnd(10))} ${chalk13.dim(r.author.padEnd(30))} ${chalk13.dim(date)}`
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
function printAuditRow(r, typeLabels) {
|
|
1818
|
+
const date = new Date(r.createdAt).toLocaleDateString("en-US", {
|
|
1819
|
+
month: "short",
|
|
1820
|
+
day: "numeric",
|
|
1821
|
+
year: "numeric",
|
|
1822
|
+
hour: "2-digit",
|
|
1823
|
+
minute: "2-digit"
|
|
1824
|
+
});
|
|
1825
|
+
const repo = r.repository ?? r.project;
|
|
1826
|
+
const label = typeLabels[r.type] ?? r.type;
|
|
1827
|
+
const typeColor = r.type === "ux-audit" ? chalk13.yellow : r.type === "security-audit" ? chalk13.red : chalk13.magenta;
|
|
1828
|
+
console.log(
|
|
1829
|
+
` ${chalk13.dim(r.id.slice(0, 8).padEnd(10))} ${typeColor(label.padEnd(12))} ${chalk13.white(repo.padEnd(40))} ${chalk13.dim(r.author.padEnd(30))} ${chalk13.dim(date)}`
|
|
1830
|
+
);
|
|
1831
|
+
if (r.branch && r.branch !== "main") {
|
|
1832
|
+
console.log(
|
|
1833
|
+
` ${" ".padEnd(10)} ${" ".padEnd(12)} ${chalk13.dim(`branch: ${r.branch} commit: ${r.commit ?? "N/A"}`)}`
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
async function auditViewCommand(id, options) {
|
|
1838
|
+
const client = await getClient();
|
|
1839
|
+
try {
|
|
1840
|
+
const audit2 = await client.audits.get(id);
|
|
1841
|
+
if (options.output) {
|
|
1842
|
+
const outputPath = path6.resolve(options.output);
|
|
1843
|
+
await fs5.ensureDir(path6.dirname(outputPath));
|
|
1844
|
+
await fs5.writeFile(outputPath, audit2.content, "utf-8");
|
|
1845
|
+
console.log(chalk13.green(`
|
|
1846
|
+
\u2713 Audit saved to ${path6.relative(process.cwd(), outputPath)}
|
|
1847
|
+
`));
|
|
1848
|
+
} else {
|
|
1849
|
+
const typeLabels = {
|
|
1850
|
+
audit: "Tech Audit",
|
|
1851
|
+
"ux-audit": "UX Audit",
|
|
1852
|
+
"security-audit": "Security Audit"
|
|
1853
|
+
};
|
|
1854
|
+
const label = typeLabels[audit2.type] ?? audit2.type;
|
|
1855
|
+
const date = new Date(audit2.createdAt).toLocaleDateString("en-US", {
|
|
1856
|
+
month: "short",
|
|
1857
|
+
day: "numeric",
|
|
1858
|
+
year: "numeric"
|
|
1859
|
+
});
|
|
1860
|
+
const repo = audit2.repository ?? audit2.project;
|
|
1861
|
+
console.log();
|
|
1862
|
+
console.log(chalk13.dim(` ${label} \xB7 ${repo} \xB7 ${audit2.author} \xB7 ${date}`));
|
|
1863
|
+
console.log();
|
|
1864
|
+
console.log(audit2.content);
|
|
206
1865
|
}
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
console.error(
|
|
1868
|
+
chalk13.red(
|
|
1869
|
+
`Failed to fetch audit: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1870
|
+
)
|
|
1871
|
+
);
|
|
1872
|
+
process.exit(1);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/commands/plan.ts
|
|
1877
|
+
import chalk14 from "chalk";
|
|
1878
|
+
import fs6 from "fs-extra";
|
|
1879
|
+
import path7 from "path";
|
|
1880
|
+
function extractFrontmatterStatus(content) {
|
|
1881
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1882
|
+
if (!match) return null;
|
|
1883
|
+
const statusMatch = match[1].match(/^status:\s*(.+)$/m);
|
|
1884
|
+
return statusMatch ? statusMatch[1].trim() : null;
|
|
1885
|
+
}
|
|
1886
|
+
async function autoDetectPlanFile() {
|
|
1887
|
+
const cwd = process.cwd();
|
|
1888
|
+
const plansDir = path7.join(cwd, ".plans");
|
|
1889
|
+
const mdFiles = await findMdFiles(plansDir);
|
|
1890
|
+
if (mdFiles.length === 0) {
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
const branch = getGitBranch();
|
|
1894
|
+
const ticketId = extractTicketId(branch);
|
|
1895
|
+
if (ticketId) {
|
|
1896
|
+
const ticketLower = ticketId.toLowerCase();
|
|
1897
|
+
const match = mdFiles.find((f) => path7.basename(f).toLowerCase().includes(ticketLower));
|
|
1898
|
+
if (match) {
|
|
1899
|
+
return match;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return mdFiles[0] ?? null;
|
|
1903
|
+
}
|
|
1904
|
+
async function planSubmitCommand(file) {
|
|
1905
|
+
const client = await getClient();
|
|
1906
|
+
let filePath;
|
|
1907
|
+
if (file) {
|
|
1908
|
+
filePath = path7.resolve(file);
|
|
1909
|
+
} else {
|
|
1910
|
+
const detected = await autoDetectPlanFile();
|
|
1911
|
+
if (!detected) {
|
|
1912
|
+
console.error(
|
|
1913
|
+
chalk14.red(
|
|
1914
|
+
"No plan file found. Provide a file path or create a plan in the .plans/ directory."
|
|
1915
|
+
)
|
|
1916
|
+
);
|
|
1917
|
+
process.exit(1);
|
|
1918
|
+
}
|
|
1919
|
+
filePath = detected;
|
|
1920
|
+
console.log(chalk14.dim(`Auto-detected plan file: ${path7.relative(process.cwd(), filePath)}`));
|
|
1921
|
+
}
|
|
1922
|
+
if (!await fs6.pathExists(filePath)) {
|
|
1923
|
+
console.error(chalk14.red(`File not found: ${filePath}`));
|
|
1924
|
+
process.exit(1);
|
|
1925
|
+
}
|
|
1926
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
1927
|
+
const filename = path7.basename(filePath);
|
|
1928
|
+
const project2 = getProjectName();
|
|
1929
|
+
const branch = getGitBranch();
|
|
1930
|
+
const commit = getGitCommit();
|
|
1931
|
+
const repo = getGitRepository();
|
|
1932
|
+
const ticket = extractTicketId(branch) ?? extractTicketId(filename);
|
|
1933
|
+
const status = extractFrontmatterStatus(content);
|
|
1934
|
+
try {
|
|
1935
|
+
const result = await client.plans.submit({
|
|
1936
|
+
project: project2,
|
|
1937
|
+
repository: repo?.slug ?? null,
|
|
1938
|
+
repositoryUrl: repo?.url ?? null,
|
|
1939
|
+
branch,
|
|
1940
|
+
commit,
|
|
1941
|
+
content,
|
|
1942
|
+
ticket: ticket ?? null,
|
|
1943
|
+
status: status ?? null,
|
|
1944
|
+
metadata: { filename }
|
|
1945
|
+
});
|
|
1946
|
+
if (result.created) {
|
|
1947
|
+
console.log(chalk14.green(`
|
|
1948
|
+
\u2713 Plan submitted! ID: ${result.id}
|
|
1949
|
+
`));
|
|
1950
|
+
} else {
|
|
1951
|
+
console.log(chalk14.green(`
|
|
1952
|
+
\u2713 Plan updated! ID: ${result.id}
|
|
1953
|
+
`));
|
|
1954
|
+
}
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
console.error(
|
|
1957
|
+
chalk14.red(
|
|
1958
|
+
`Failed to submit plan: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1959
|
+
)
|
|
1960
|
+
);
|
|
1961
|
+
process.exit(1);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
async function planListCommand(options) {
|
|
1965
|
+
const client = await getClient();
|
|
1966
|
+
try {
|
|
1967
|
+
const body = await client.plans.list({
|
|
1968
|
+
repository: options.repository,
|
|
1969
|
+
author: options.author,
|
|
1970
|
+
limit: options.limit ? parseInt(options.limit, 10) : void 0
|
|
1971
|
+
});
|
|
1972
|
+
if (body.plans.length === 0) {
|
|
1973
|
+
console.log(chalk14.dim("\n No plans found.\n"));
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
console.log(chalk14.bold(`
|
|
1977
|
+
Plans (${body.count}):
|
|
1978
|
+
`));
|
|
1979
|
+
console.log(
|
|
1980
|
+
chalk14.dim(
|
|
1981
|
+
` ${"TICKET".padEnd(14)} ${"STATUS".padEnd(12)} ${"REPOSITORY".padEnd(36)} ${"AUTHOR".padEnd(30)} UPDATED`
|
|
1982
|
+
)
|
|
1983
|
+
);
|
|
1984
|
+
console.log();
|
|
1985
|
+
const statusColors = {
|
|
1986
|
+
submitted: chalk14.dim,
|
|
1987
|
+
approved: chalk14.cyan,
|
|
1988
|
+
validated: chalk14.green,
|
|
1989
|
+
reviewed: chalk14.magenta
|
|
1990
|
+
};
|
|
1991
|
+
for (const p of body.plans) {
|
|
1992
|
+
const date = new Date(p.updatedAt).toLocaleDateString("en-US", {
|
|
1993
|
+
month: "short",
|
|
1994
|
+
day: "numeric",
|
|
1995
|
+
year: "numeric",
|
|
1996
|
+
hour: "2-digit",
|
|
1997
|
+
minute: "2-digit"
|
|
1998
|
+
});
|
|
1999
|
+
const repo = p.repository ?? p.project;
|
|
2000
|
+
const ticket = p.ticket ?? "-";
|
|
2001
|
+
const colorFn = statusColors[p.status] ?? chalk14.dim;
|
|
2002
|
+
console.log(
|
|
2003
|
+
` ${chalk14.white(ticket.padEnd(14))} ${colorFn(p.status.padEnd(12))} ${chalk14.white(repo.padEnd(36))} ${chalk14.dim(p.author.padEnd(30))} ${chalk14.dim(date)}`
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
console.log();
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
2009
|
+
console.error(chalk14.red(`Failed to list plans: ${msg}`));
|
|
2010
|
+
process.exit(1);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// src/index.ts
|
|
2015
|
+
init_update();
|
|
2016
|
+
|
|
2017
|
+
// src/utils/debug.ts
|
|
2018
|
+
import chalk15 from "chalk";
|
|
2019
|
+
function debugLog(message) {
|
|
2020
|
+
if (process.env["MELTCTL_DEBUG"]) {
|
|
2021
|
+
console.error(chalk15.dim(`[debug] ${message}`));
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// src/utils/analytics.ts
|
|
2026
|
+
init_version();
|
|
2027
|
+
async function trackCommand(command, success, errorMessage) {
|
|
2028
|
+
try {
|
|
2029
|
+
if (process.env["MELTCTL_NO_ANALYTICS"]) {
|
|
2030
|
+
debugLog("Analytics disabled via MELTCTL_NO_ANALYTICS");
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
const auth = await getStoredAuth();
|
|
2034
|
+
if (!auth || new Date(auth.expiresAt) <= /* @__PURE__ */ new Date()) {
|
|
2035
|
+
debugLog("Analytics skipped: not authenticated or token expired");
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const repo = getGitRepository();
|
|
2039
|
+
const client = createMeltClient({ baseUrl: API_BASE, token: auth.token });
|
|
2040
|
+
const controller = new AbortController();
|
|
2041
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
2042
|
+
try {
|
|
2043
|
+
await client.events.submit({
|
|
2044
|
+
command,
|
|
2045
|
+
project: getProjectName(),
|
|
2046
|
+
repository: repo?.slug ?? null,
|
|
2047
|
+
branch: getGitBranch(),
|
|
2048
|
+
version: CLI_VERSION,
|
|
2049
|
+
success,
|
|
2050
|
+
errorMessage: errorMessage?.slice(0, 500) ?? null
|
|
2051
|
+
});
|
|
2052
|
+
debugLog(`Analytics event sent for "${command}"`);
|
|
2053
|
+
} catch (e) {
|
|
2054
|
+
debugLog(`Analytics error: ${e instanceof Error ? e.message : e}`);
|
|
2055
|
+
}
|
|
2056
|
+
clearTimeout(timeout);
|
|
2057
|
+
} catch (e) {
|
|
2058
|
+
debugLog(`Analytics error: ${e instanceof Error ? e.message : e}`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/index.ts
|
|
2063
|
+
init_version();
|
|
2064
|
+
var program = new Command();
|
|
2065
|
+
program.name("meltctl").description(
|
|
2066
|
+
"Set up AI-first development standards (AGENTS.md, Claude skills, Cursor commands, OpenCode commands, MCP config) in your project.\n\nRequires a @meltstudio.co Google Workspace account. Run `npx @meltstudio/meltctl@latest login` first, then `npx @meltstudio/meltctl@latest project init` in your repo."
|
|
2067
|
+
).version(CLI_VERSION).addHelpText("beforeAll", () => {
|
|
2068
|
+
printBanner();
|
|
2069
|
+
return "";
|
|
2070
|
+
}).addHelpText(
|
|
2071
|
+
"after",
|
|
2072
|
+
`
|
|
2073
|
+
${chalk16.bold("AI Skills")} ${chalk16.dim("(run these in your AI coding tool, not the CLI):")}
|
|
2074
|
+
${chalk16.dim(" /melt-setup Analyze the project and customize AGENTS.md")}
|
|
2075
|
+
${chalk16.dim(" /melt-link Connect and verify ticket tracker + browser testing")}
|
|
2076
|
+
${chalk16.dim(" /melt-plan Design an implementation approach before writing code")}
|
|
2077
|
+
${chalk16.dim(" /melt-validate Run the validation plan and verify end-to-end")}
|
|
2078
|
+
${chalk16.dim(" /melt-review Review changes against project standards")}
|
|
2079
|
+
${chalk16.dim(" /melt-pr Create a well-structured pull request")}
|
|
2080
|
+
${chalk16.dim(" /melt-debug Systematically investigate and fix bugs")}
|
|
2081
|
+
${chalk16.dim(" /melt-audit Run a project compliance audit")}
|
|
2082
|
+
${chalk16.dim(" /melt-ux-audit Review UI against usability heuristics")}
|
|
2083
|
+
${chalk16.dim(" /melt-security-audit Run a security posture audit across the platform")}
|
|
2084
|
+
${chalk16.dim(" /melt-update Update Melt skills to the latest version")}
|
|
2085
|
+
${chalk16.dim(" /melt-help Answer questions about the development playbook")}
|
|
2086
|
+
`
|
|
2087
|
+
).hook("preAction", async () => {
|
|
2088
|
+
await checkAndEnforceUpdate();
|
|
2089
|
+
}).hook("postAction", async (_thisCommand, actionCommand) => {
|
|
2090
|
+
try {
|
|
2091
|
+
const parts = [];
|
|
2092
|
+
let cmd = actionCommand;
|
|
2093
|
+
while (cmd && cmd.name() !== "meltctl") {
|
|
2094
|
+
parts.unshift(cmd.name());
|
|
2095
|
+
cmd = cmd.parent;
|
|
2096
|
+
}
|
|
2097
|
+
await trackCommand(parts.join(" "), true);
|
|
2098
|
+
} catch {
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
program.command("login").description("authenticate with Google Workspace (opens browser)").action(async () => {
|
|
2102
|
+
await loginCommand();
|
|
2103
|
+
});
|
|
2104
|
+
program.command("logout").description("clear stored credentials from ~/.meltctl/").action(async () => {
|
|
2105
|
+
await logoutCommand();
|
|
2106
|
+
});
|
|
2107
|
+
var project = program.command("project").description("manage project configuration").addHelpText(
|
|
2108
|
+
"after",
|
|
2109
|
+
`
|
|
2110
|
+
${chalk16.dim("Related skills (run in your AI coding tool after init):")}
|
|
2111
|
+
${chalk16.dim(" /melt-setup Analyze the project and customize AGENTS.md")}
|
|
2112
|
+
${chalk16.dim(" /melt-link Connect and verify ticket tracker + browser testing")}
|
|
2113
|
+
${chalk16.dim(" /melt-update Update Melt skills to the latest version")}
|
|
2114
|
+
`
|
|
2115
|
+
);
|
|
2116
|
+
project.command("init").description(
|
|
2117
|
+
"scaffold Melt development tools into the current directory (AGENTS.md, .claude/, .cursor/, .opencode/, .mcp.json)"
|
|
2118
|
+
).option("--force", "overwrite existing files if already initialized").option("--claude", "generate Claude Code configuration").option("--cursor", "generate Cursor configuration").option("--opencode", "generate OpenCode configuration").action((options) => {
|
|
2119
|
+
return initCommand({
|
|
2120
|
+
force: options.force,
|
|
2121
|
+
claude: options.claude,
|
|
2122
|
+
cursor: options.cursor,
|
|
2123
|
+
opencode: options.opencode
|
|
2124
|
+
});
|
|
2125
|
+
});
|
|
2126
|
+
project.command("templates").description("fetch latest templates to a temp directory (for /melt-update)").action(async () => {
|
|
2127
|
+
await templatesCommand();
|
|
2128
|
+
});
|
|
2129
|
+
program.command("standup").description("submit your daily standup report").addOption(new Option("--yesterday <text>").hideHelp()).addOption(new Option("--today <text>").hideHelp()).addOption(new Option("--blockers <text>").hideHelp()).action(async (options) => {
|
|
2130
|
+
await standupCommand(options);
|
|
2131
|
+
});
|
|
2132
|
+
program.command("feedback").description("send feedback and coins to a teammate").addOption(new Option("--to <userId>").hideHelp()).addOption(new Option("--coins <amount>").hideHelp()).addOption(new Option("--description <text>").hideHelp()).action(async (options) => {
|
|
2133
|
+
await feedbackCommand(options);
|
|
2134
|
+
});
|
|
2135
|
+
program.command("coins").description("check your coin balance or the team leaderboard").option("--leaderboard", "show the team leaderboard").action(async (options) => {
|
|
2136
|
+
await coinsCommand(options);
|
|
2137
|
+
});
|
|
2138
|
+
var audit = program.command("audit").description("submit and list audits").addHelpText(
|
|
2139
|
+
"after",
|
|
2140
|
+
`
|
|
2141
|
+
${chalk16.dim("Related skills (run in your AI coding tool):")}
|
|
2142
|
+
${chalk16.dim(" /melt-audit Run a project compliance audit")}
|
|
2143
|
+
${chalk16.dim(" /melt-ux-audit Review UI against usability heuristics")}
|
|
2144
|
+
${chalk16.dim(" /melt-security-audit Run a security posture audit across the platform")}
|
|
2145
|
+
`
|
|
2146
|
+
);
|
|
2147
|
+
audit.command("submit").description("submit an audit report from a markdown file").argument("[file]", "path to the audit file (auto-detects from .audits/ if omitted)").action(async (file) => {
|
|
2148
|
+
await auditSubmitCommand(file);
|
|
2149
|
+
});
|
|
2150
|
+
audit.command("list").description("list submitted audits (Team Managers only)").option("--type <type>", "filter by type (audit, ux-audit, security-audit)").option("--repository <repo>", "filter by repository (owner/repo)").option("--latest", "show only the latest audit per project and type").option("--limit <n>", "max results (default 50, max 200)").action(async (options) => {
|
|
2151
|
+
await auditListCommand(options);
|
|
2152
|
+
});
|
|
2153
|
+
audit.command("view").description("view a submitted audit by ID (Team Managers only)").argument("<id>", "audit ID").option("-o, --output <file>", "save to file instead of printing").action(async (id, options) => {
|
|
2154
|
+
await auditViewCommand(id, options);
|
|
2155
|
+
});
|
|
2156
|
+
var plan = program.command("plan").description("submit and list plans").addHelpText(
|
|
2157
|
+
"after",
|
|
2158
|
+
`
|
|
2159
|
+
${chalk16.dim("Related skill (run in your AI coding tool):")}
|
|
2160
|
+
${chalk16.dim(" /melt-plan Design an implementation approach before writing code")}
|
|
2161
|
+
`
|
|
2162
|
+
);
|
|
2163
|
+
plan.command("submit").description("submit or update a plan from a markdown file").argument("[file]", "path to the plan file (auto-detects from .plans/ if omitted)").action(async (file) => {
|
|
2164
|
+
await planSubmitCommand(file);
|
|
2165
|
+
});
|
|
2166
|
+
plan.command("list").description("list submitted plans (Team Managers only)").option("--repository <repo>", "filter by repository (owner/repo)").option("--author <email>", "filter by author email").option("--limit <n>", "max results (default 50, max 200)").action(async (options) => {
|
|
2167
|
+
await planListCommand(options);
|
|
2168
|
+
});
|
|
2169
|
+
program.command("update").description("update meltctl to the latest version").action(async () => {
|
|
2170
|
+
await updateCommand();
|
|
2171
|
+
});
|
|
2172
|
+
program.command("version").description("show current version").option("--check", "also check for available updates").action(async (options) => {
|
|
2173
|
+
if (options.check) {
|
|
2174
|
+
await versionCheckCommand();
|
|
2175
|
+
} else {
|
|
2176
|
+
console.log(CLI_VERSION);
|
|
2177
|
+
}
|
|
207
2178
|
});
|
|
208
2179
|
program.parseAsync(process.argv).catch((error) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
trackCommand(commandName, false, message);
|
|
215
|
-
console.error(chalk.red(message));
|
|
216
|
-
process.exit(1);
|
|
2180
|
+
const message = error instanceof Error ? error.message : "An unexpected error occurred";
|
|
2181
|
+
const commandName = process.argv.slice(2).filter((a) => !a.startsWith("-")).join(" ") || "unknown";
|
|
2182
|
+
trackCommand(commandName, false, message);
|
|
2183
|
+
console.error(chalk16.red(message));
|
|
2184
|
+
process.exit(1);
|
|
217
2185
|
});
|