@way_marks/cli 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.js +140 -67
- package/dist/commands/list.js +46 -0
- package/dist/commands/open.js +78 -0
- package/dist/commands/pause.js +77 -0
- package/dist/commands/resume.js +77 -0
- package/dist/commands/start.js +40 -13
- package/dist/commands/stop.js +12 -0
- package/dist/index.js +17 -1
- package/dist/registry.js +275 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -40,7 +40,8 @@ const os = __importStar(require("os"));
|
|
|
40
40
|
const readline = __importStar(require("readline"));
|
|
41
41
|
const child_process_1 = require("child_process");
|
|
42
42
|
const DEFAULT_CONFIG = {
|
|
43
|
-
version: '
|
|
43
|
+
version: '2',
|
|
44
|
+
platforms: ['claude'],
|
|
44
45
|
policies: {
|
|
45
46
|
allowedPaths: [
|
|
46
47
|
'./src/**',
|
|
@@ -170,6 +171,38 @@ function resolveServerBin() {
|
|
|
170
171
|
return path.resolve(__dirname, '../../../server/dist/mcp/server.js');
|
|
171
172
|
}
|
|
172
173
|
}
|
|
174
|
+
async function selectPlatforms() {
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log('┌─ Platform Selection ──────────────────────────────────┐');
|
|
177
|
+
console.log('│ Which AI platform(s) will you use with Waymark? │');
|
|
178
|
+
console.log('│ │');
|
|
179
|
+
console.log('│ 1. Claude Desktop / Claude Code (RECOMMENDED) ✓ │');
|
|
180
|
+
console.log('│ → Best experience. Full Waymark features. │');
|
|
181
|
+
console.log('│ │');
|
|
182
|
+
console.log('│ 2. GitHub Copilot CLI (EXPERIMENTAL) ⚠ │');
|
|
183
|
+
console.log('│ → Terminal support. CLI-only (no VSCode). │');
|
|
184
|
+
console.log('│ │');
|
|
185
|
+
console.log('│ 3. Both (Claude + GitHub Copilot CLI) │');
|
|
186
|
+
console.log('│ → Setup for both. Easy to switch. │');
|
|
187
|
+
console.log('└───────────────────────────────────────────────────────┘');
|
|
188
|
+
console.log('');
|
|
189
|
+
while (true) {
|
|
190
|
+
const answer = await prompt('Enter choice (1-3) [default: 1]: ');
|
|
191
|
+
const choice = answer || '1';
|
|
192
|
+
if (choice === '1') {
|
|
193
|
+
return ['claude'];
|
|
194
|
+
}
|
|
195
|
+
else if (choice === '2') {
|
|
196
|
+
return ['copilot-cli'];
|
|
197
|
+
}
|
|
198
|
+
else if (choice === '3') {
|
|
199
|
+
return ['claude', 'copilot-cli'];
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.log('Invalid choice. Please enter 1, 2, or 3.');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
173
206
|
async function run() {
|
|
174
207
|
const projectRoot = process.cwd();
|
|
175
208
|
const projectName = kebabCase(path.basename(projectRoot));
|
|
@@ -183,6 +216,8 @@ async function run() {
|
|
|
183
216
|
if (!hasPackageJson && !hasGit) {
|
|
184
217
|
console.warn('Warning: No package.json or .git found. Continuing anyway.');
|
|
185
218
|
}
|
|
219
|
+
// Step 1b — Select platforms (NEW in Phase 5)
|
|
220
|
+
const selectedPlatforms = await selectPlatforms();
|
|
186
221
|
// Step 2 — Install @way_marks/server (skip if already resolvable or in monorepo)
|
|
187
222
|
let serverBin;
|
|
188
223
|
try {
|
|
@@ -208,38 +243,44 @@ async function run() {
|
|
|
208
243
|
serverBin = resolveServerBin();
|
|
209
244
|
}
|
|
210
245
|
}
|
|
211
|
-
// Step 3 — Create waymark.config.json
|
|
246
|
+
// Step 3 — Create waymark.config.json with selected platforms
|
|
212
247
|
const configPath = path.join(projectRoot, 'waymark.config.json');
|
|
248
|
+
const platformConfig = { ...DEFAULT_CONFIG, platforms: selectedPlatforms };
|
|
213
249
|
if (fs.existsSync(configPath)) {
|
|
214
250
|
const answer = await prompt('waymark.config.json exists. Overwrite? (y/N) ');
|
|
215
251
|
if (answer.toLowerCase() !== 'y') {
|
|
216
252
|
console.log('Keeping existing waymark.config.json');
|
|
217
253
|
}
|
|
218
254
|
else {
|
|
219
|
-
fs.writeFileSync(configPath, JSON.stringify(
|
|
220
|
-
console.log('✓ Created waymark.config.json');
|
|
255
|
+
fs.writeFileSync(configPath, JSON.stringify(platformConfig, null, 2) + '\n');
|
|
256
|
+
console.log('✓ Created waymark.config.json with platforms:', selectedPlatforms.join(', '));
|
|
221
257
|
}
|
|
222
258
|
}
|
|
223
259
|
else {
|
|
224
|
-
fs.writeFileSync(configPath, JSON.stringify(
|
|
225
|
-
console.log('✓ Created waymark.config.json');
|
|
260
|
+
fs.writeFileSync(configPath, JSON.stringify(platformConfig, null, 2) + '\n');
|
|
261
|
+
console.log('✓ Created waymark.config.json with platforms:', selectedPlatforms.join(', '));
|
|
226
262
|
}
|
|
227
|
-
// Step 4 — Create/append CLAUDE.md
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
263
|
+
// Step 4 — Create/append CLAUDE.md (only if Claude selected)
|
|
264
|
+
if (selectedPlatforms.includes('claude')) {
|
|
265
|
+
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
266
|
+
const claudeMdContent = generateClaudeMd(projectName, defaultPort);
|
|
267
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
268
|
+
const existing = fs.readFileSync(claudeMdPath, 'utf8');
|
|
269
|
+
if (existing.includes(WAYMARK_MARKER)) {
|
|
270
|
+
console.log('✓ CLAUDE.md already has Waymark section');
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
fs.appendFileSync(claudeMdPath, `\n${WAYMARK_MARKER}\n${claudeMdContent}`);
|
|
274
|
+
console.log('✓ Appended Waymark section to CLAUDE.md');
|
|
275
|
+
}
|
|
234
276
|
}
|
|
235
277
|
else {
|
|
236
|
-
fs.
|
|
237
|
-
console.log('✓
|
|
278
|
+
fs.writeFileSync(claudeMdPath, `${WAYMARK_MARKER}\n${claudeMdContent}`);
|
|
279
|
+
console.log('✓ Created CLAUDE.md — Claude Code will now use Waymark automatically');
|
|
238
280
|
}
|
|
239
281
|
}
|
|
240
282
|
else {
|
|
241
|
-
|
|
242
|
-
console.log('✓ Created CLAUDE.md — Claude Code will now use Waymark automatically');
|
|
283
|
+
console.log('⊘ Skipping CLAUDE.md (Claude not selected)');
|
|
243
284
|
}
|
|
244
285
|
// Step 5 — Update .gitignore
|
|
245
286
|
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
@@ -256,47 +297,66 @@ async function run() {
|
|
|
256
297
|
else {
|
|
257
298
|
console.log('✓ .gitignore already up to date');
|
|
258
299
|
}
|
|
259
|
-
// Step 6 — Register MCP in both Claude configs
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
fs.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
desktopConfig.mcpServers
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
300
|
+
// Step 6 — Register MCP in both Claude configs (only if Claude selected)
|
|
301
|
+
if (selectedPlatforms.includes('claude')) {
|
|
302
|
+
const nodeBin = process.execPath;
|
|
303
|
+
const mcpEntry = {
|
|
304
|
+
command: nodeBin,
|
|
305
|
+
args: [serverBin, '--project-root', projectRoot, '--db-path', dbPath]
|
|
306
|
+
};
|
|
307
|
+
// Claude Desktop config — add/update this project's entry only
|
|
308
|
+
const desktopConfigPath = getClaudeDesktopConfigPath();
|
|
309
|
+
try {
|
|
310
|
+
const desktopDir = path.dirname(desktopConfigPath);
|
|
311
|
+
if (!fs.existsSync(desktopDir))
|
|
312
|
+
fs.mkdirSync(desktopDir, { recursive: true });
|
|
313
|
+
const desktopConfig = fs.existsSync(desktopConfigPath)
|
|
314
|
+
? JSON.parse(fs.readFileSync(desktopConfigPath, 'utf8'))
|
|
315
|
+
: { mcpServers: {} };
|
|
316
|
+
if (!desktopConfig.mcpServers)
|
|
317
|
+
desktopConfig.mcpServers = {};
|
|
318
|
+
desktopConfig.mcpServers[mcpKey] = mcpEntry;
|
|
319
|
+
fs.writeFileSync(desktopConfigPath, JSON.stringify(desktopConfig, null, 2) + '\n');
|
|
320
|
+
console.log(`✓ Registered MCP server "${mcpKey}" in Claude Desktop config`);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
console.warn(`Warning: Could not update Claude Desktop config: ${err.message}`);
|
|
324
|
+
}
|
|
325
|
+
// .mcp.json (Claude Code project-level)
|
|
326
|
+
const mcpJsonPath = path.join(projectRoot, '.mcp.json');
|
|
327
|
+
try {
|
|
328
|
+
const mcpJson = fs.existsSync(mcpJsonPath)
|
|
329
|
+
? JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'))
|
|
330
|
+
: { mcpServers: {} };
|
|
331
|
+
if (!mcpJson.mcpServers)
|
|
332
|
+
mcpJson.mcpServers = {};
|
|
333
|
+
mcpJson.mcpServers[mcpKey] = { type: 'stdio', ...mcpEntry, cwd: projectRoot };
|
|
334
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + '\n');
|
|
335
|
+
console.log(`✓ Created/updated .mcp.json for Claude Code`);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
console.warn(`Warning: Could not update .mcp.json: ${err.message}`);
|
|
339
|
+
}
|
|
282
340
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const mcpJson = fs.existsSync(mcpJsonPath)
|
|
287
|
-
? JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'))
|
|
288
|
-
: { mcpServers: {} };
|
|
289
|
-
if (!mcpJson.mcpServers)
|
|
290
|
-
mcpJson.mcpServers = {};
|
|
291
|
-
mcpJson.mcpServers[mcpKey] = { type: 'stdio', ...mcpEntry, cwd: projectRoot };
|
|
292
|
-
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + '\n');
|
|
293
|
-
console.log(`✓ Created/updated .mcp.json for Claude Code`);
|
|
341
|
+
else {
|
|
342
|
+
console.log('⊘ Skipping MCP registration (Claude not selected)');
|
|
294
343
|
}
|
|
295
|
-
|
|
296
|
-
|
|
344
|
+
// Step 6b — Show Copilot CLI instructions (if Copilot CLI selected)
|
|
345
|
+
if (selectedPlatforms.includes('copilot-cli')) {
|
|
346
|
+
console.log('');
|
|
347
|
+
console.log('📋 GitHub Copilot CLI Setup');
|
|
348
|
+
console.log('─'.repeat(50));
|
|
349
|
+
console.log('⚠️ Copilot CLI support requires manual setup.');
|
|
350
|
+
console.log('See COPILOT_CLI.md for step-by-step instructions.');
|
|
351
|
+
console.log('');
|
|
352
|
+
console.log('Quick start:');
|
|
353
|
+
console.log('1. Find copilot binary: which copilot');
|
|
354
|
+
console.log('2. Run: npx @way_marks/cli init-copilot-wrapper');
|
|
355
|
+
console.log('3. Test: copilot --version');
|
|
356
|
+
console.log('');
|
|
297
357
|
}
|
|
298
|
-
// Step 7 — Success summary
|
|
299
|
-
const col =
|
|
358
|
+
// Step 7 — Success summary (updated for platform selection)
|
|
359
|
+
const col = 50;
|
|
300
360
|
const pad = (s) => s + ' '.repeat(Math.max(0, col - 2 - s.length));
|
|
301
361
|
console.log('');
|
|
302
362
|
console.log('┌' + '─'.repeat(col) + '┐');
|
|
@@ -304,22 +364,35 @@ async function run() {
|
|
|
304
364
|
console.log(`│ ${pad('')} │`);
|
|
305
365
|
console.log(`│ ${pad(`Project: ${projectName}`)} │`);
|
|
306
366
|
console.log(`│ ${pad('Database: .waymark/waymark.db')} │`);
|
|
307
|
-
console.log(`│ ${pad(`
|
|
367
|
+
console.log(`│ ${pad(`Platforms: ${selectedPlatforms.join(', ')}`)} │`);
|
|
308
368
|
console.log(`│ ${pad('')} │`);
|
|
309
369
|
console.log(`│ ${pad('Files created:')} │`);
|
|
310
|
-
console.log(`│ ${pad(' waymark.config.json')} │`);
|
|
311
|
-
|
|
370
|
+
console.log(`│ ${pad(' waymark.config.json (with platforms)')} │`);
|
|
371
|
+
if (selectedPlatforms.includes('claude')) {
|
|
372
|
+
console.log(`│ ${pad(' CLAUDE.md')} │`);
|
|
373
|
+
}
|
|
374
|
+
if (selectedPlatforms.includes('copilot-cli')) {
|
|
375
|
+
console.log(`│ ${pad(' (see COPILOT_CLI.md setup)')} │`);
|
|
376
|
+
}
|
|
312
377
|
console.log(`│ ${pad(' .waymark/ (gitignored)')} │`);
|
|
313
378
|
console.log(`│ ${pad('')} │`);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
379
|
+
if (selectedPlatforms.includes('claude')) {
|
|
380
|
+
console.log(`│ ${pad('Next steps (Claude):')} │`);
|
|
381
|
+
console.log(`│ ${pad('1. Run: npx @way_marks/cli start')} │`);
|
|
382
|
+
console.log(`│ ${pad('2. Restart Claude Code')} │`);
|
|
383
|
+
console.log(`│ ${pad('3. Open project in Claude')} │`);
|
|
384
|
+
console.log(`│ ${pad('4. Dashboard: http://localhost:3001')} │`);
|
|
385
|
+
}
|
|
386
|
+
if (selectedPlatforms.includes('copilot-cli')) {
|
|
387
|
+
if (selectedPlatforms.includes('claude')) {
|
|
388
|
+
console.log(`│ ${pad('')} │`);
|
|
389
|
+
}
|
|
390
|
+
console.log(`│ ${pad('Next steps (Copilot CLI):')} │`);
|
|
391
|
+
console.log(`│ ${pad('1. See COPILOT_CLI.md for wrapper setup')} │`);
|
|
392
|
+
}
|
|
320
393
|
console.log(`│ ${pad('')} │`);
|
|
321
|
-
console.log(`│ ${pad('
|
|
322
|
-
console.log(`│ ${pad('
|
|
323
|
-
console.log(`│ ${pad('
|
|
394
|
+
console.log(`│ ${pad('For more info:')} │`);
|
|
395
|
+
console.log(`│ ${pad(' README.md — Feature overview')} │`);
|
|
396
|
+
console.log(`│ ${pad(' README_PLATFORMS.md — Platform support')} │`);
|
|
324
397
|
console.log('└' + '─'.repeat(col) + '┘');
|
|
325
398
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* waymark list — List all registered Waymark projects
|
|
4
|
+
*
|
|
5
|
+
* Shows project name, port, status, uptime, and user.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.run = run;
|
|
9
|
+
const registry_1 = require("../registry");
|
|
10
|
+
function run() {
|
|
11
|
+
(0, registry_1.cleanupStaleEntries)();
|
|
12
|
+
const projects = (0, registry_1.listProjects)();
|
|
13
|
+
if (projects.length === 0) {
|
|
14
|
+
console.log('No Waymark projects registered.');
|
|
15
|
+
console.log('Run: waymark init && waymark start');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
// Format output
|
|
19
|
+
console.log('\n📋 Waymark Projects');
|
|
20
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
21
|
+
for (const p of projects) {
|
|
22
|
+
const statusEmoji = p.status === 'running' ? '🟢' : p.status === 'paused' ? '⏸️ ' : '🔴';
|
|
23
|
+
const started = new Date(p.startedAt);
|
|
24
|
+
const uptime = p.status === 'running'
|
|
25
|
+
? Math.floor((Date.now() - started.getTime()) / 1000)
|
|
26
|
+
: 0;
|
|
27
|
+
const uptimeStr = uptime > 3600
|
|
28
|
+
? `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`
|
|
29
|
+
: `${Math.floor(uptime / 60)}m`;
|
|
30
|
+
console.log(`${statusEmoji} ${p.projectName}`);
|
|
31
|
+
console.log(` ID: ${p.id}`);
|
|
32
|
+
console.log(` Port: http://localhost:${p.port}`);
|
|
33
|
+
console.log(` Status: ${p.status}${p.status === 'running' ? ` (${uptimeStr})` : ''}`);
|
|
34
|
+
console.log(` User: ${p.user}@${p.hostname}`);
|
|
35
|
+
console.log(` Path: ${p.projectRoot}`);
|
|
36
|
+
console.log('');
|
|
37
|
+
}
|
|
38
|
+
const running = projects.filter(p => p.status === 'running').length;
|
|
39
|
+
const paused = projects.filter(p => p.status === 'paused').length;
|
|
40
|
+
const stopped = projects.filter(p => p.status === 'stopped').length;
|
|
41
|
+
console.log(`Summary: ${running} running, ${paused} paused, ${stopped} stopped`);
|
|
42
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
43
|
+
if (running === 0) {
|
|
44
|
+
console.log('💡 Tip: Run "waymark open PROJECT_NAME" to start a project');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* waymark open — Open a project dashboard in browser
|
|
4
|
+
*
|
|
5
|
+
* Usage: waymark open PROJECT_NAME
|
|
6
|
+
*
|
|
7
|
+
* If the project is not running, starts it first.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.run = run;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const registry_1 = require("../registry");
|
|
13
|
+
function openBrowser(url) {
|
|
14
|
+
try {
|
|
15
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
16
|
+
: process.platform === 'win32' ? 'start'
|
|
17
|
+
: 'xdg-open';
|
|
18
|
+
(0, child_process_1.execSync)(`${cmd} ${url}`, { stdio: 'ignore' });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
console.log(`Open this URL in your browser: ${url}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function run() {
|
|
25
|
+
const projectName = process.argv[3];
|
|
26
|
+
if (!projectName) {
|
|
27
|
+
console.error('Usage: waymark open PROJECT_NAME');
|
|
28
|
+
console.error('');
|
|
29
|
+
console.error('Example:');
|
|
30
|
+
console.error(' waymark open my-app');
|
|
31
|
+
console.error('');
|
|
32
|
+
console.error('Registered projects:');
|
|
33
|
+
(0, registry_1.cleanupStaleEntries)();
|
|
34
|
+
const projects = (0, registry_1.listProjects)('running');
|
|
35
|
+
if (projects.length > 0) {
|
|
36
|
+
projects.forEach(p => {
|
|
37
|
+
console.error(` - ${p.id} (port ${p.port})`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.error(' (none running — run: waymark start)');
|
|
42
|
+
}
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
(0, registry_1.cleanupStaleEntries)();
|
|
46
|
+
const project = (0, registry_1.getProject)(projectName);
|
|
47
|
+
if (!project) {
|
|
48
|
+
console.error(`Project not found: ${projectName}`);
|
|
49
|
+
console.error('');
|
|
50
|
+
console.error('Available projects:');
|
|
51
|
+
const all = (0, registry_1.listProjects)();
|
|
52
|
+
if (all.length > 0) {
|
|
53
|
+
all.forEach(p => {
|
|
54
|
+
const status = p.status === 'running' ? '🟢' : '🔴';
|
|
55
|
+
console.error(` ${status} ${p.id} (port ${p.port})`);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.error(' (none registered)');
|
|
60
|
+
}
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (project.status !== 'running') {
|
|
64
|
+
console.log(`Project "${projectName}" is ${project.status}. Starting...`);
|
|
65
|
+
// Try to start it by running `waymark start` in its directory
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
try {
|
|
68
|
+
process.chdir(project.projectRoot);
|
|
69
|
+
(0, child_process_1.execSync)('npx @way_marks/cli start', { stdio: 'inherit' });
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
process.chdir(cwd);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const url = `http://localhost:${project.port}`;
|
|
76
|
+
console.log(`Opening dashboard: ${url}`);
|
|
77
|
+
openBrowser(url);
|
|
78
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* waymark pause — Pause a project (keep port allocation, but mark as paused)
|
|
4
|
+
*
|
|
5
|
+
* Usage: waymark pause [PROJECT_NAME]
|
|
6
|
+
*
|
|
7
|
+
* If PROJECT_NAME is omitted, pauses the current project.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.run = run;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const registry_1 = require("../registry");
|
|
47
|
+
function run() {
|
|
48
|
+
const projectName = process.argv[3];
|
|
49
|
+
const projectRoot = process.cwd();
|
|
50
|
+
const configPath = path.join(projectRoot, '.waymark', 'config.json');
|
|
51
|
+
if (!fs.existsSync(configPath)) {
|
|
52
|
+
console.error('No .waymark/config.json found. Run: waymark init');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
56
|
+
const name = projectName || config.projectName || path.basename(projectRoot);
|
|
57
|
+
try {
|
|
58
|
+
const project = (0, registry_1.getProject)(name);
|
|
59
|
+
if (!project) {
|
|
60
|
+
console.error(`Project not found: ${name}`);
|
|
61
|
+
console.error('Hint: Use "waymark list" to see registered projects');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (project.status === 'paused') {
|
|
65
|
+
console.log(`Project "${name}" is already paused.`);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
(0, registry_1.updateProjectStatus)(name, 'paused');
|
|
69
|
+
console.log(`✓ Paused project: ${name}`);
|
|
70
|
+
console.log(` Port allocated: ${project.port} (reserved for this project)`);
|
|
71
|
+
console.log(` Resume with: waymark resume ${name}`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error(`Failed to pause project: ${err.message}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* waymark resume — Resume a paused project
|
|
4
|
+
*
|
|
5
|
+
* Usage: waymark resume [PROJECT_NAME]
|
|
6
|
+
*
|
|
7
|
+
* If PROJECT_NAME is omitted, resumes the current project.
|
|
8
|
+
* The project keeps its previously allocated port.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.run = run;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const registry_1 = require("../registry");
|
|
48
|
+
function run() {
|
|
49
|
+
const projectName = process.argv[3];
|
|
50
|
+
const projectRoot = process.cwd();
|
|
51
|
+
const configPath = path.join(projectRoot, '.waymark', 'config.json');
|
|
52
|
+
if (!fs.existsSync(configPath)) {
|
|
53
|
+
console.error('No .waymark/config.json found. Run: waymark init');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
57
|
+
const name = projectName || config.projectName || path.basename(projectRoot);
|
|
58
|
+
try {
|
|
59
|
+
const project = (0, registry_1.getProject)(name);
|
|
60
|
+
if (!project) {
|
|
61
|
+
console.error(`Project not found: ${name}`);
|
|
62
|
+
console.error('Hint: Use "waymark list" to see registered projects');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (project.status !== 'paused') {
|
|
66
|
+
console.error(`Project "${name}" is not paused (current status: ${project.status})`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
(0, registry_1.updateProjectStatus)(name, 'running');
|
|
70
|
+
console.log(`✓ Resumed project: ${name}`);
|
|
71
|
+
console.log(` Dashboard: http://localhost:${project.port}`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error(`Failed to resume project: ${err.message}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/commands/start.js
CHANGED
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const net = __importStar(require("net"));
|
|
40
40
|
const child_process_1 = require("child_process");
|
|
41
|
+
const registry_1 = require("../registry");
|
|
41
42
|
function resolveServerBin(name) {
|
|
42
43
|
const file = name === 'mcp' ? 'mcp/server.js' : 'api/server.js';
|
|
43
44
|
try {
|
|
@@ -76,20 +77,27 @@ function kebabCase(str) {
|
|
|
76
77
|
.replace(/^-|-$/g, '');
|
|
77
78
|
}
|
|
78
79
|
function findAvailablePort(preferred) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
// Try registry first (Phase 2+)
|
|
81
|
+
try {
|
|
82
|
+
return Promise.resolve((0, registry_1.findAvailablePort)(preferred));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Fallback: old logic (Phase 1 compatibility)
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const server = net.createServer();
|
|
88
|
+
server.listen(preferred, () => {
|
|
89
|
+
const port = server.address().port;
|
|
90
|
+
server.close(() => resolve(port));
|
|
91
|
+
});
|
|
92
|
+
server.on('error', () => {
|
|
93
|
+
if (preferred >= 4000) {
|
|
94
|
+
console.error('No available ports found between 3001-4000. Stop other Waymark projects first.');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
resolve(findAvailablePort(preferred + 1));
|
|
98
|
+
});
|
|
91
99
|
});
|
|
92
|
-
}
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
async function run() {
|
|
95
103
|
const projectRoot = process.cwd();
|
|
@@ -157,6 +165,25 @@ async function run() {
|
|
|
157
165
|
port,
|
|
158
166
|
startedAt: new Date().toISOString()
|
|
159
167
|
}, null, 2) + '\n');
|
|
168
|
+
// Register in global registry (Phase 2+)
|
|
169
|
+
try {
|
|
170
|
+
(0, registry_1.registerProject)({
|
|
171
|
+
id: projectName,
|
|
172
|
+
projectRoot,
|
|
173
|
+
projectName,
|
|
174
|
+
port,
|
|
175
|
+
mcp_pid: mcpProc.pid,
|
|
176
|
+
api_pid: apiProc.pid,
|
|
177
|
+
status: 'running',
|
|
178
|
+
startedAt: new Date().toISOString(),
|
|
179
|
+
hostname: require('os').hostname(),
|
|
180
|
+
user: process.env.USER || 'unknown',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.warn('Warning: failed to register in global registry:', err instanceof Error ? err.message : String(err));
|
|
185
|
+
// Continue anyway — registry is optional (backward compat)
|
|
186
|
+
}
|
|
160
187
|
// Open browser after short delay for server startup
|
|
161
188
|
setTimeout(() => {
|
|
162
189
|
openBrowser(`http://localhost:${port}`);
|
package/dist/commands/stop.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.run = run;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const registry_1 = require("../registry");
|
|
39
40
|
function tryKill(pid) {
|
|
40
41
|
try {
|
|
41
42
|
process.kill(pid, 'SIGTERM');
|
|
@@ -66,6 +67,17 @@ function run() {
|
|
|
66
67
|
fs.unlinkSync(pidFile);
|
|
67
68
|
}
|
|
68
69
|
catch { /* already gone */ }
|
|
70
|
+
// Unregister from global registry and release port (Phase 2+ & Phase 4)
|
|
71
|
+
try {
|
|
72
|
+
const project = (0, registry_1.findProjectByPath)(process.cwd());
|
|
73
|
+
if (project) {
|
|
74
|
+
(0, registry_1.releasePort)(project.id); // Phase 4: Release port for reuse
|
|
75
|
+
(0, registry_1.unregisterProject)(project.id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
// ignore — registry cleanup is optional
|
|
80
|
+
}
|
|
69
81
|
if (killedApi || killedMcp) {
|
|
70
82
|
console.log('Waymark stopped.');
|
|
71
83
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,20 +11,36 @@ switch (command) {
|
|
|
11
11
|
case 'stop':
|
|
12
12
|
require('./commands/stop').run();
|
|
13
13
|
break;
|
|
14
|
+
case 'pause':
|
|
15
|
+
require('./commands/pause').run();
|
|
16
|
+
break;
|
|
17
|
+
case 'resume':
|
|
18
|
+
require('./commands/resume').run();
|
|
19
|
+
break;
|
|
14
20
|
case 'status':
|
|
15
21
|
require('./commands/status').run();
|
|
16
22
|
break;
|
|
17
23
|
case 'logs':
|
|
18
24
|
require('./commands/logs').run();
|
|
19
25
|
break;
|
|
26
|
+
case 'list':
|
|
27
|
+
require('./commands/list').run();
|
|
28
|
+
break;
|
|
29
|
+
case 'open':
|
|
30
|
+
require('./commands/open').run();
|
|
31
|
+
break;
|
|
20
32
|
default:
|
|
21
|
-
console.log('Usage: npx @way_marks/cli <init|start|stop|status|logs>');
|
|
33
|
+
console.log('Usage: npx @way_marks/cli <init|start|stop|pause|resume|status|logs|list|open>');
|
|
22
34
|
console.log('');
|
|
23
35
|
console.log('Commands:');
|
|
24
36
|
console.log(' init Set up Waymark in the current project');
|
|
25
37
|
console.log(' start Start the Waymark dashboard and MCP server');
|
|
26
38
|
console.log(' stop Stop the running Waymark servers');
|
|
39
|
+
console.log(' pause Pause a project (keep port allocated)');
|
|
40
|
+
console.log(' resume Resume a paused project');
|
|
27
41
|
console.log(' status Show current Waymark status and pending count');
|
|
28
42
|
console.log(' logs Show recent action log');
|
|
43
|
+
console.log(' list List all registered Waymark projects');
|
|
44
|
+
console.log(' open Open a project dashboard or start it');
|
|
29
45
|
process.exit(command ? 1 : 0);
|
|
30
46
|
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Waymark Project Registry
|
|
4
|
+
*
|
|
5
|
+
* Central registry for managing active Waymark projects.
|
|
6
|
+
* Stored at ~/.waymark/registry.json
|
|
7
|
+
*
|
|
8
|
+
* This enables:
|
|
9
|
+
* - `waymark list` — enumerate all active projects
|
|
10
|
+
* - `waymark open PROJECT` — quickly switch between projects
|
|
11
|
+
* - Port broker — central allocation system (Phase 2 future)
|
|
12
|
+
* - Project lifecycle tracking
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.getRegistry = getRegistry;
|
|
49
|
+
exports.registerProject = registerProject;
|
|
50
|
+
exports.unregisterProject = unregisterProject;
|
|
51
|
+
exports.updateProjectStatus = updateProjectStatus;
|
|
52
|
+
exports.releasePort = releasePort;
|
|
53
|
+
exports.getProject = getProject;
|
|
54
|
+
exports.findProjectByPath = findProjectByPath;
|
|
55
|
+
exports.listProjects = listProjects;
|
|
56
|
+
exports.findAvailablePort = findAvailablePort;
|
|
57
|
+
exports.cleanupStaleEntries = cleanupStaleEntries;
|
|
58
|
+
exports.garbageCollectRegistry = garbageCollectRegistry;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
const os = __importStar(require("os"));
|
|
62
|
+
const REGISTRY_DIR = path.join(os.homedir(), '.waymark');
|
|
63
|
+
const REGISTRY_PATH = path.join(REGISTRY_DIR, 'registry.json');
|
|
64
|
+
/**
|
|
65
|
+
* Ensure registry directory and file exist
|
|
66
|
+
*/
|
|
67
|
+
function ensureRegistry() {
|
|
68
|
+
if (!fs.existsSync(REGISTRY_DIR)) {
|
|
69
|
+
fs.mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
if (!fs.existsSync(REGISTRY_PATH)) {
|
|
72
|
+
const empty = {
|
|
73
|
+
version: 1,
|
|
74
|
+
projects: {},
|
|
75
|
+
releasedPorts: [],
|
|
76
|
+
lastUpdated: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(empty, null, 2) + '\n');
|
|
79
|
+
return empty;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const reg = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
|
83
|
+
// Ensure releasedPorts exists for Phase 4
|
|
84
|
+
if (!reg.releasedPorts) {
|
|
85
|
+
reg.releasedPorts = [];
|
|
86
|
+
}
|
|
87
|
+
return reg;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
throw new Error(`Failed to read registry: ${err instanceof Error ? err.message : String(err)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get the current registry
|
|
95
|
+
*/
|
|
96
|
+
function getRegistry() {
|
|
97
|
+
return ensureRegistry();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Register a project (called on `waymark start`)
|
|
101
|
+
*/
|
|
102
|
+
function registerProject(entry) {
|
|
103
|
+
const registry = ensureRegistry();
|
|
104
|
+
entry.startedAt = entry.startedAt || new Date().toISOString();
|
|
105
|
+
entry.status = entry.status || 'running';
|
|
106
|
+
entry.hostname = entry.hostname || os.hostname();
|
|
107
|
+
entry.user = entry.user || process.env.USER || 'unknown';
|
|
108
|
+
registry.projects[entry.id] = entry;
|
|
109
|
+
registry.lastUpdated = new Date().toISOString();
|
|
110
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Unregister a project (called on `waymark stop`)
|
|
114
|
+
*/
|
|
115
|
+
function unregisterProject(id) {
|
|
116
|
+
const registry = ensureRegistry();
|
|
117
|
+
delete registry.projects[id];
|
|
118
|
+
registry.lastUpdated = new Date().toISOString();
|
|
119
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Update project status
|
|
123
|
+
*/
|
|
124
|
+
function updateProjectStatus(id, status) {
|
|
125
|
+
const registry = ensureRegistry();
|
|
126
|
+
const entry = registry.projects[id];
|
|
127
|
+
if (!entry) {
|
|
128
|
+
throw new Error(`Project not found: ${id}`);
|
|
129
|
+
}
|
|
130
|
+
entry.status = status;
|
|
131
|
+
if (status === 'stopped') {
|
|
132
|
+
entry.stoppedAt = new Date().toISOString();
|
|
133
|
+
}
|
|
134
|
+
else if (status === 'paused') {
|
|
135
|
+
entry.pausedAt = new Date().toISOString();
|
|
136
|
+
}
|
|
137
|
+
registry.lastUpdated = new Date().toISOString();
|
|
138
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Phase 4: Release a port when project stops (enables reuse)
|
|
142
|
+
*/
|
|
143
|
+
function releasePort(projectId) {
|
|
144
|
+
const registry = ensureRegistry();
|
|
145
|
+
const entry = registry.projects[projectId];
|
|
146
|
+
if (!entry) {
|
|
147
|
+
return; // Project not found, nothing to do
|
|
148
|
+
}
|
|
149
|
+
// Add port to released queue for reuse
|
|
150
|
+
if (!registry.releasedPorts) {
|
|
151
|
+
registry.releasedPorts = [];
|
|
152
|
+
}
|
|
153
|
+
registry.releasedPorts.push(entry.port);
|
|
154
|
+
registry.lastUpdated = new Date().toISOString();
|
|
155
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get a project by ID
|
|
159
|
+
*/
|
|
160
|
+
function getProject(id) {
|
|
161
|
+
const registry = ensureRegistry();
|
|
162
|
+
return registry.projects[id] || null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Find a project by directory path
|
|
166
|
+
*/
|
|
167
|
+
function findProjectByPath(projectRoot) {
|
|
168
|
+
const registry = ensureRegistry();
|
|
169
|
+
const normalized = path.resolve(projectRoot);
|
|
170
|
+
for (const entry of Object.values(registry.projects)) {
|
|
171
|
+
if (path.resolve(entry.projectRoot) === normalized) {
|
|
172
|
+
return entry;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* List all projects
|
|
179
|
+
*/
|
|
180
|
+
function listProjects(filter) {
|
|
181
|
+
const registry = ensureRegistry();
|
|
182
|
+
const entries = Object.values(registry.projects);
|
|
183
|
+
if (filter) {
|
|
184
|
+
return entries.filter(e => e.status === filter);
|
|
185
|
+
}
|
|
186
|
+
return entries;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Find available port in the 3001-4000 range
|
|
190
|
+
* (Phase 2 future: will be replaced by central port broker)
|
|
191
|
+
*/
|
|
192
|
+
function findAvailablePort(preferred = 3001) {
|
|
193
|
+
const registry = ensureRegistry();
|
|
194
|
+
const usedPorts = new Set(Object.values(registry.projects)
|
|
195
|
+
.filter(p => p.status === 'running')
|
|
196
|
+
.map(p => p.port));
|
|
197
|
+
// Phase 4: Check if we have a released port to reuse first
|
|
198
|
+
if (registry.releasedPorts && registry.releasedPorts.length > 0) {
|
|
199
|
+
const releasedPort = registry.releasedPorts.shift();
|
|
200
|
+
if (releasedPort && !usedPorts.has(releasedPort)) {
|
|
201
|
+
// Save updated registry with shifted port
|
|
202
|
+
registry.lastUpdated = new Date().toISOString();
|
|
203
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
204
|
+
return releasedPort;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Check if preferred is available
|
|
208
|
+
if (!usedPorts.has(preferred)) {
|
|
209
|
+
return preferred;
|
|
210
|
+
}
|
|
211
|
+
// Find next available
|
|
212
|
+
for (let port = 3001; port <= 4000; port++) {
|
|
213
|
+
if (!usedPorts.has(port)) {
|
|
214
|
+
return port;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
throw new Error('No available ports in range 3001-4000');
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Clean up stale entries (processes that are dead but not unregistered)
|
|
221
|
+
* Called periodically by waymark list/status commands
|
|
222
|
+
*/
|
|
223
|
+
function cleanupStaleEntries() {
|
|
224
|
+
const registry = ensureRegistry();
|
|
225
|
+
let changed = false;
|
|
226
|
+
for (const [id, entry] of Object.entries(registry.projects)) {
|
|
227
|
+
if (entry.status === 'running' && entry.mcp_pid) {
|
|
228
|
+
try {
|
|
229
|
+
process.kill(entry.mcp_pid, 0); // Check if process exists
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Process is dead — mark as stopped
|
|
233
|
+
entry.status = 'stopped';
|
|
234
|
+
entry.stoppedAt = new Date().toISOString();
|
|
235
|
+
changed = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (changed) {
|
|
240
|
+
registry.lastUpdated = new Date().toISOString();
|
|
241
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Phase 4: Garbage collect stale entries (cleanup old stopped projects)
|
|
246
|
+
* Removes entries older than daysOld that are marked as stopped
|
|
247
|
+
*/
|
|
248
|
+
function garbageCollectRegistry(daysOld = 7) {
|
|
249
|
+
const registry = ensureRegistry();
|
|
250
|
+
const cutoffTime = new Date();
|
|
251
|
+
cutoffTime.setDate(cutoffTime.getDate() - daysOld);
|
|
252
|
+
let removed = 0;
|
|
253
|
+
const idsToRemove = [];
|
|
254
|
+
for (const [id, entry] of Object.entries(registry.projects)) {
|
|
255
|
+
if (entry.status === 'stopped' && entry.stoppedAt) {
|
|
256
|
+
const stoppedTime = new Date(entry.stoppedAt);
|
|
257
|
+
if (stoppedTime < cutoffTime) {
|
|
258
|
+
idsToRemove.push(id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const id of idsToRemove) {
|
|
263
|
+
delete registry.projects[id];
|
|
264
|
+
removed++;
|
|
265
|
+
}
|
|
266
|
+
if (removed > 0) {
|
|
267
|
+
// Clean up released ports queue (keep only last 20)
|
|
268
|
+
if (registry.releasedPorts && registry.releasedPorts.length > 20) {
|
|
269
|
+
registry.releasedPorts = registry.releasedPorts.slice(-20);
|
|
270
|
+
}
|
|
271
|
+
registry.lastUpdated = new Date().toISOString();
|
|
272
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
273
|
+
}
|
|
274
|
+
return removed;
|
|
275
|
+
}
|