@way_marks/cli 2.0.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.js +7 -7
- package/dist/commands/logs.js +59 -3
- package/dist/commands/start.js +143 -6
- package/dist/index.js +14 -9
- package/dist/registry.js +81 -11
- package/package.json +2 -2
package/dist/commands/init.js
CHANGED
|
@@ -83,7 +83,7 @@ function kebabCase(str) {
|
|
|
83
83
|
.replace(/-+/g, '-')
|
|
84
84
|
.replace(/^-|-$/g, '');
|
|
85
85
|
}
|
|
86
|
-
function generateClaudeMd(projectName
|
|
86
|
+
function generateClaudeMd(projectName) {
|
|
87
87
|
return `---
|
|
88
88
|
# ⚠️ WAYMARK ACTIVE — MANDATORY INSTRUCTIONS
|
|
89
89
|
|
|
@@ -135,9 +135,10 @@ Then restart Claude Code and try again."
|
|
|
135
135
|
|
|
136
136
|
## Dashboard
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
Pending and recent actions are visible in the
|
|
139
|
+
Waymark dashboard. Run \`npx @way_marks/cli status\`
|
|
140
|
+
to see the current dashboard URL for this project.
|
|
141
|
+
Approve pending actions there. Roll back any write there.
|
|
141
142
|
|
|
142
143
|
## This file was generated by Waymark
|
|
143
144
|
Do not delete or modify this file.
|
|
@@ -208,7 +209,6 @@ async function run() {
|
|
|
208
209
|
const projectName = kebabCase(path.basename(projectRoot));
|
|
209
210
|
const mcpKey = `waymark-${projectName}`;
|
|
210
211
|
const dbPath = path.join(projectRoot, '.waymark', 'waymark.db');
|
|
211
|
-
const defaultPort = 3001;
|
|
212
212
|
console.log(`Initializing Waymark in: ${projectRoot}`);
|
|
213
213
|
// Step 1 — Detect project
|
|
214
214
|
const hasPackageJson = fs.existsSync(path.join(projectRoot, 'package.json'));
|
|
@@ -263,7 +263,7 @@ async function run() {
|
|
|
263
263
|
// Step 4 — Create/append CLAUDE.md (only if Claude selected)
|
|
264
264
|
if (selectedPlatforms.includes('claude')) {
|
|
265
265
|
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
266
|
-
const claudeMdContent = generateClaudeMd(projectName
|
|
266
|
+
const claudeMdContent = generateClaudeMd(projectName);
|
|
267
267
|
if (fs.existsSync(claudeMdPath)) {
|
|
268
268
|
const existing = fs.readFileSync(claudeMdPath, 'utf8');
|
|
269
269
|
if (existing.includes(WAYMARK_MARKER)) {
|
|
@@ -379,9 +379,9 @@ async function run() {
|
|
|
379
379
|
if (selectedPlatforms.includes('claude')) {
|
|
380
380
|
console.log(`│ ${pad('Next steps (Claude):')} │`);
|
|
381
381
|
console.log(`│ ${pad('1. Run: npx @way_marks/cli start')} │`);
|
|
382
|
+
console.log(`│ ${pad(' (it will print the dashboard URL)')} │`);
|
|
382
383
|
console.log(`│ ${pad('2. Restart Claude Code')} │`);
|
|
383
384
|
console.log(`│ ${pad('3. Open project in Claude')} │`);
|
|
384
|
-
console.log(`│ ${pad('4. Dashboard: http://localhost:3001')} │`);
|
|
385
385
|
}
|
|
386
386
|
if (selectedPlatforms.includes('copilot-cli')) {
|
|
387
387
|
if (selectedPlatforms.includes('claude')) {
|
package/dist/commands/logs.js
CHANGED
|
@@ -1,7 +1,58 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.run = run;
|
|
4
|
-
const
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the dashboard URL for the current project by reading the live port
|
|
41
|
+
* out of `<cwd>/.waymark/config.json` (written by `waymark start`).
|
|
42
|
+
* Returns null when the project hasn't been started yet.
|
|
43
|
+
*/
|
|
44
|
+
function resolveBaseUrl() {
|
|
45
|
+
const configPath = path.join(process.cwd(), '.waymark', 'config.json');
|
|
46
|
+
if (!fs.existsSync(configPath))
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
50
|
+
return cfg.port ? `http://localhost:${cfg.port}` : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
5
56
|
function parseIso(iso) {
|
|
6
57
|
const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T');
|
|
7
58
|
const withZ = normalized.endsWith('Z') ? normalized : normalized + 'Z';
|
|
@@ -34,15 +85,20 @@ async function run() {
|
|
|
34
85
|
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) || 20 : 20;
|
|
35
86
|
const pendingOnly = args.includes('--pending');
|
|
36
87
|
const blockedOnly = args.includes('--blocked');
|
|
88
|
+
const base = resolveBaseUrl();
|
|
89
|
+
if (!base) {
|
|
90
|
+
console.log('Waymark is not running for this project. Start with: npx @way_marks/cli start');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
37
93
|
let rows;
|
|
38
94
|
try {
|
|
39
|
-
const res = await fetch(`${
|
|
95
|
+
const res = await fetch(`${base}/api/actions`);
|
|
40
96
|
if (!res.ok)
|
|
41
97
|
throw new Error('Bad response');
|
|
42
98
|
rows = await res.json();
|
|
43
99
|
}
|
|
44
100
|
catch {
|
|
45
|
-
console.log(
|
|
101
|
+
console.log(`Waymark is not reachable at ${base}. Start with: npx @way_marks/cli start`);
|
|
46
102
|
return;
|
|
47
103
|
}
|
|
48
104
|
if (pendingOnly)
|
package/dist/commands/start.js
CHANGED
|
@@ -82,7 +82,7 @@ function findAvailablePort(preferred) {
|
|
|
82
82
|
return Promise.resolve((0, registry_1.findAvailablePort)(preferred));
|
|
83
83
|
}
|
|
84
84
|
catch {
|
|
85
|
-
// Fallback:
|
|
85
|
+
// Fallback: kernel-level probe (registry corrupt or absent)
|
|
86
86
|
return new Promise((resolve) => {
|
|
87
87
|
const server = net.createServer();
|
|
88
88
|
server.listen(preferred, () => {
|
|
@@ -90,8 +90,9 @@ function findAvailablePort(preferred) {
|
|
|
90
90
|
server.close(() => resolve(port));
|
|
91
91
|
});
|
|
92
92
|
server.on('error', () => {
|
|
93
|
-
if (preferred >=
|
|
94
|
-
console.error(
|
|
93
|
+
if (preferred >= registry_1.PORT_RANGE_END) {
|
|
94
|
+
console.error(`No available ports found between ${registry_1.PORT_RANGE_START}-${registry_1.PORT_RANGE_END}. ` +
|
|
95
|
+
`Stop other Waymark projects first.`);
|
|
95
96
|
process.exit(1);
|
|
96
97
|
}
|
|
97
98
|
resolve(findAvailablePort(preferred + 1));
|
|
@@ -99,6 +100,37 @@ function findAvailablePort(preferred) {
|
|
|
99
100
|
});
|
|
100
101
|
}
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Probe whether `port` is currently free. We listen exactly the same way the
|
|
105
|
+
* real server does (no explicit host → all interfaces, dual-stack), so this
|
|
106
|
+
* matches what would happen at actual bind time and catches IPv4/IPv6
|
|
107
|
+
* dual-stack collisions.
|
|
108
|
+
*/
|
|
109
|
+
function isPortFree(port) {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const server = net.createServer();
|
|
112
|
+
server.once('error', () => resolve(false));
|
|
113
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
114
|
+
server.listen(port);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parse `--port <n>` from process.argv. Returns null when not provided.
|
|
119
|
+
* Validates the value is a positive integer < 65536. Exits with a readable
|
|
120
|
+
* error on garbage input rather than silently ignoring.
|
|
121
|
+
*/
|
|
122
|
+
function parsePortFlag(argv) {
|
|
123
|
+
const idx = argv.indexOf('--port');
|
|
124
|
+
if (idx === -1)
|
|
125
|
+
return null;
|
|
126
|
+
const raw = argv[idx + 1];
|
|
127
|
+
const port = Number.parseInt(raw, 10);
|
|
128
|
+
if (!Number.isInteger(port) || port <= 0 || port >= 65536 || String(port) !== raw) {
|
|
129
|
+
console.error(`Invalid --port value: "${raw}". Expected an integer 1-65535.`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
return port;
|
|
133
|
+
}
|
|
102
134
|
async function run() {
|
|
103
135
|
const projectRoot = process.cwd();
|
|
104
136
|
const configPath = path.join(projectRoot, 'waymark.config.json');
|
|
@@ -113,7 +145,7 @@ async function run() {
|
|
|
113
145
|
try {
|
|
114
146
|
const saved = JSON.parse(fs.readFileSync(pidFile, 'utf8'));
|
|
115
147
|
if (isAlive(saved.api) || isAlive(saved.mcp)) {
|
|
116
|
-
const port = saved.port ||
|
|
148
|
+
const port = saved.port || registry_1.PORT_RANGE_START;
|
|
117
149
|
console.log('Waymark is already running.');
|
|
118
150
|
console.log(`Dashboard: http://localhost:${port}`);
|
|
119
151
|
console.log('Run "npx @way_marks/cli stop" to stop it.');
|
|
@@ -124,9 +156,96 @@ async function run() {
|
|
|
124
156
|
// stale/corrupt PID file — continue to start
|
|
125
157
|
}
|
|
126
158
|
}
|
|
127
|
-
const port = await findAvailablePort(3001);
|
|
128
|
-
const dbPath = path.join(projectRoot, '.waymark', 'waymark.db');
|
|
129
159
|
const projectName = kebabCase(path.basename(projectRoot));
|
|
160
|
+
const dbPath = path.join(projectRoot, '.waymark', 'waymark.db');
|
|
161
|
+
// Load project config — used for the optional `port` pin.
|
|
162
|
+
let projectConfig = {};
|
|
163
|
+
try {
|
|
164
|
+
projectConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
console.error(`Failed to parse waymark.config.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
// Port resolution precedence: --port flag > config.port > auto-allocate.
|
|
171
|
+
const flagPort = parsePortFlag(process.argv.slice(3));
|
|
172
|
+
const configPort = typeof projectConfig.port === 'number' ? projectConfig.port : null;
|
|
173
|
+
const pinnedPort = flagPort ?? configPort;
|
|
174
|
+
const pinnedSource = flagPort !== null ? 'flag' : configPort !== null ? 'config' : null;
|
|
175
|
+
let port;
|
|
176
|
+
if (pinnedPort !== null) {
|
|
177
|
+
if (!(await isPortFree(pinnedPort))) {
|
|
178
|
+
const existing = (0, registry_1.findProjectByPath)(projectRoot);
|
|
179
|
+
const owner = (() => {
|
|
180
|
+
// Best-effort: try to identify which Waymark project owns the port.
|
|
181
|
+
// We can't reach into other processes; just advise `waymark list`.
|
|
182
|
+
return null;
|
|
183
|
+
})();
|
|
184
|
+
console.error(`Port ${pinnedPort} is already in use ` +
|
|
185
|
+
`(pinned via ${pinnedSource === 'flag' ? '--port flag' : 'waymark.config.json'}).\n` +
|
|
186
|
+
` → Run "npx @way_marks/cli list" to see other Waymark projects.\n` +
|
|
187
|
+
` → Or remove the pin to auto-allocate from ${registry_1.PORT_RANGE_START}-${registry_1.PORT_RANGE_END}.`);
|
|
188
|
+
void existing;
|
|
189
|
+
void owner; // reserved for future "owner is project X" diagnostics
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
port = pinnedPort;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// Migration notice: if this project's prior registry entry used a legacy port,
|
|
196
|
+
// the auto-allocator may pick a fresh modern port instead. Emit a one-line
|
|
197
|
+
// notice so the user understands why their bookmark might shift.
|
|
198
|
+
const prior = (0, registry_1.findProjectByPath)(projectRoot);
|
|
199
|
+
const newPort = await findAvailablePort(registry_1.PORT_RANGE_START);
|
|
200
|
+
if (prior &&
|
|
201
|
+
typeof prior.port === 'number' &&
|
|
202
|
+
prior.port < registry_1.LEGACY_PORT_BOUNDARY &&
|
|
203
|
+
newPort !== prior.port) {
|
|
204
|
+
console.log(`[waymark] Reallocating from legacy port :${prior.port} to :${newPort}.\n` +
|
|
205
|
+
` Set "port": ${prior.port} in waymark.config.json to keep the old one.`);
|
|
206
|
+
}
|
|
207
|
+
port = newPort;
|
|
208
|
+
}
|
|
209
|
+
// Pre-flight: surface any project-id collision *before* spawning children,
|
|
210
|
+
// so we never leave orphan processes when registration would fail.
|
|
211
|
+
const colliding = (() => {
|
|
212
|
+
try {
|
|
213
|
+
// Probe by performing a fake registerProject() that we immediately undo
|
|
214
|
+
// would risk corrupting the file; instead read directly via findProjectByPath
|
|
215
|
+
// and check the registry's keying contract here.
|
|
216
|
+
const otherAtSamePath = (0, registry_1.findProjectByPath)(projectRoot);
|
|
217
|
+
if (otherAtSamePath && otherAtSamePath.id === projectName)
|
|
218
|
+
return null;
|
|
219
|
+
// Different path with same id → ask registry for the entry by id.
|
|
220
|
+
// findProjectByPath already returned null, so we need a different lookup.
|
|
221
|
+
// Use require-time access to the same module's getProject.
|
|
222
|
+
const reg = require('../registry');
|
|
223
|
+
const sameId = reg.getProject(projectName);
|
|
224
|
+
if (sameId &&
|
|
225
|
+
path.resolve(sameId.projectRoot) !== path.resolve(projectRoot) &&
|
|
226
|
+
((sameId.mcp_pid != null && (() => { try {
|
|
227
|
+
process.kill(sameId.mcp_pid, 0);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
} })()) ||
|
|
233
|
+
sameId.status === 'running')) {
|
|
234
|
+
return sameId;
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
})();
|
|
242
|
+
if (colliding) {
|
|
243
|
+
console.error(`Another running Waymark project named "${projectName}" is registered at ${colliding.projectRoot}.\n` +
|
|
244
|
+
`This start request is at ${projectRoot}.\n` +
|
|
245
|
+
` → Stop the other project first ("npx @way_marks/cli stop" in ${colliding.projectRoot}),\n` +
|
|
246
|
+
` → or rename one of the directories so the project ids differ.`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
130
249
|
const nodeBin = process.execPath;
|
|
131
250
|
const mcpBin = resolveServerBin('mcp');
|
|
132
251
|
const apiBin = resolveServerBin('api');
|
|
@@ -181,6 +300,24 @@ async function run() {
|
|
|
181
300
|
});
|
|
182
301
|
}
|
|
183
302
|
catch (err) {
|
|
303
|
+
if (err instanceof registry_1.ProjectIdCollisionError) {
|
|
304
|
+
// Race: another start landed between our pre-flight and registration.
|
|
305
|
+
// Tear down the children we just spawned so we don't leak processes.
|
|
306
|
+
try {
|
|
307
|
+
apiProc.pid && process.kill(apiProc.pid);
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
try {
|
|
311
|
+
mcpProc.pid && process.kill(mcpProc.pid);
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
try {
|
|
315
|
+
fs.unlinkSync(pidFile);
|
|
316
|
+
}
|
|
317
|
+
catch { }
|
|
318
|
+
console.error(err.message);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
184
321
|
console.warn('Warning: failed to register in global registry:', err instanceof Error ? err.message : String(err));
|
|
185
322
|
// Continue anyway — registry is optional (backward compat)
|
|
186
323
|
}
|
package/dist/index.js
CHANGED
|
@@ -33,14 +33,19 @@ switch (command) {
|
|
|
33
33
|
console.log('Usage: npx @way_marks/cli <init|start|stop|pause|resume|status|logs|list|open>');
|
|
34
34
|
console.log('');
|
|
35
35
|
console.log('Commands:');
|
|
36
|
-
console.log(' init
|
|
37
|
-
console.log(' start
|
|
38
|
-
console.log(' stop
|
|
39
|
-
console.log(' pause
|
|
40
|
-
console.log(' resume
|
|
41
|
-
console.log(' status
|
|
42
|
-
console.log(' logs
|
|
43
|
-
console.log(' list
|
|
44
|
-
console.log(' open
|
|
36
|
+
console.log(' init Set up Waymark in the current project');
|
|
37
|
+
console.log(' start [--port <n>] Start the Waymark dashboard and MCP server');
|
|
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');
|
|
41
|
+
console.log(' status Show current Waymark status and pending count');
|
|
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');
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('Notes:');
|
|
47
|
+
console.log(' • Default port range is 47000-47999 (avoids collisions with dev servers).');
|
|
48
|
+
console.log(' • Pin a port per-project: add "port": 47100 to waymark.config.json.');
|
|
49
|
+
console.log(' • Override at runtime: waymark start --port 47200');
|
|
45
50
|
process.exit(command ? 1 : 0);
|
|
46
51
|
}
|
package/dist/registry.js
CHANGED
|
@@ -45,6 +45,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
45
|
};
|
|
46
46
|
})();
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.LEGACY_PORT_BOUNDARY = exports.PORT_RANGE_END = exports.PORT_RANGE_START = exports.ProjectIdCollisionError = void 0;
|
|
48
49
|
exports.getRegistry = getRegistry;
|
|
49
50
|
exports.registerProject = registerProject;
|
|
50
51
|
exports.unregisterProject = unregisterProject;
|
|
@@ -96,11 +97,51 @@ function ensureRegistry() {
|
|
|
96
97
|
function getRegistry() {
|
|
97
98
|
return ensureRegistry();
|
|
98
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Error thrown when two projects with the same kebab-cased basename are running
|
|
102
|
+
* at different paths. Caller (start.ts) catches this and emits a user-facing
|
|
103
|
+
* message instead of silently overwriting the registry.
|
|
104
|
+
*/
|
|
105
|
+
class ProjectIdCollisionError extends Error {
|
|
106
|
+
constructor(id, existingRoot, newRoot) {
|
|
107
|
+
super(`Another running Waymark project named "${id}" is registered at ${existingRoot}. ` +
|
|
108
|
+
`This start request is at ${newRoot}. ` +
|
|
109
|
+
`Stop the other project first ("npx @way_marks/cli stop" in ${existingRoot}), ` +
|
|
110
|
+
`or rename one of the directories so the project ids differ.`);
|
|
111
|
+
this.name = 'ProjectIdCollisionError';
|
|
112
|
+
this.id = id;
|
|
113
|
+
this.existingRoot = existingRoot;
|
|
114
|
+
this.newRoot = newRoot;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.ProjectIdCollisionError = ProjectIdCollisionError;
|
|
99
118
|
/**
|
|
100
119
|
* Register a project (called on `waymark start`)
|
|
120
|
+
*
|
|
121
|
+
* Surfaces a `ProjectIdCollisionError` when an entry with the same `id`
|
|
122
|
+
* already exists at a *different* path AND its process is still alive.
|
|
123
|
+
* Same-path re-registration (the resume case) overwrites silently.
|
|
124
|
+
* Stale entries (process gone) are also overwritten — caller will start fresh.
|
|
101
125
|
*/
|
|
102
126
|
function registerProject(entry) {
|
|
103
127
|
const registry = ensureRegistry();
|
|
128
|
+
const existing = registry.projects[entry.id];
|
|
129
|
+
if (existing && path.resolve(existing.projectRoot) !== path.resolve(entry.projectRoot)) {
|
|
130
|
+
const aliveByPid = existing.mcp_pid != null && (() => {
|
|
131
|
+
try {
|
|
132
|
+
process.kill(existing.mcp_pid, 0);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
const aliveByStatus = existing.status === 'running';
|
|
140
|
+
if (aliveByPid || aliveByStatus) {
|
|
141
|
+
throw new ProjectIdCollisionError(entry.id, existing.projectRoot, entry.projectRoot);
|
|
142
|
+
}
|
|
143
|
+
// Stale entry at a different path — fall through and overwrite.
|
|
144
|
+
}
|
|
104
145
|
entry.startedAt = entry.startedAt || new Date().toISOString();
|
|
105
146
|
entry.status = entry.status || 'running';
|
|
106
147
|
entry.hostname = entry.hostname || os.hostname();
|
|
@@ -186,35 +227,64 @@ function listProjects(filter) {
|
|
|
186
227
|
return entries;
|
|
187
228
|
}
|
|
188
229
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
230
|
+
* Default port range for new Waymark projects.
|
|
231
|
+
*
|
|
232
|
+
* 47000-47999 is in the IANA "user ports" range and avoids collisions with the
|
|
233
|
+
* most common dev-server defaults (3000 Next.js, 3001 fallback, 5173 Vite,
|
|
234
|
+
* 4200 Angular, 8080 generic, 1337 Strapi, etc.). Existing projects on the
|
|
235
|
+
* legacy 3001-4000 range keep working until they're stopped — see start.ts
|
|
236
|
+
* for the migration notice.
|
|
191
237
|
*/
|
|
192
|
-
|
|
238
|
+
exports.PORT_RANGE_START = 47000;
|
|
239
|
+
exports.PORT_RANGE_END = 47999;
|
|
240
|
+
exports.LEGACY_PORT_BOUNDARY = 47000; // anything below this is "legacy"
|
|
241
|
+
/**
|
|
242
|
+
* Find an available port for a new Waymark instance.
|
|
243
|
+
*
|
|
244
|
+
* Order:
|
|
245
|
+
* 1. Reuse a freed port from the releasedPorts queue (when not in use).
|
|
246
|
+
* 2. Use `preferred` if free.
|
|
247
|
+
* 3. Scan 47000..47999 for the first free port.
|
|
248
|
+
*/
|
|
249
|
+
function findAvailablePort(preferred = exports.PORT_RANGE_START) {
|
|
193
250
|
const registry = ensureRegistry();
|
|
194
251
|
const usedPorts = new Set(Object.values(registry.projects)
|
|
195
252
|
.filter(p => p.status === 'running')
|
|
196
253
|
.map(p => p.port));
|
|
197
|
-
//
|
|
254
|
+
// Reuse a released port if one is available — but skip any legacy ports
|
|
255
|
+
// (< LEGACY_PORT_BOUNDARY) so the v3.1 migration cleanly transitions every
|
|
256
|
+
// project off the old range over time. Legacy ports are simply discarded.
|
|
198
257
|
if (registry.releasedPorts && registry.releasedPorts.length > 0) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
258
|
+
let mutated = false;
|
|
259
|
+
while (registry.releasedPorts.length > 0) {
|
|
260
|
+
const candidate = registry.releasedPorts.shift();
|
|
261
|
+
mutated = true;
|
|
262
|
+
if (candidate == null)
|
|
263
|
+
continue;
|
|
264
|
+
if (candidate < exports.LEGACY_PORT_BOUNDARY)
|
|
265
|
+
continue; // drop legacy
|
|
266
|
+
if (usedPorts.has(candidate))
|
|
267
|
+
continue;
|
|
268
|
+
registry.lastUpdated = new Date().toISOString();
|
|
269
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
270
|
+
return candidate;
|
|
271
|
+
}
|
|
272
|
+
if (mutated) {
|
|
202
273
|
registry.lastUpdated = new Date().toISOString();
|
|
203
274
|
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
204
|
-
return releasedPort;
|
|
205
275
|
}
|
|
206
276
|
}
|
|
207
277
|
// Check if preferred is available
|
|
208
278
|
if (!usedPorts.has(preferred)) {
|
|
209
279
|
return preferred;
|
|
210
280
|
}
|
|
211
|
-
// Find next available
|
|
212
|
-
for (let port =
|
|
281
|
+
// Find next available in the modern range
|
|
282
|
+
for (let port = exports.PORT_RANGE_START; port <= exports.PORT_RANGE_END; port++) {
|
|
213
283
|
if (!usedPorts.has(port)) {
|
|
214
284
|
return port;
|
|
215
285
|
}
|
|
216
286
|
}
|
|
217
|
-
throw new Error(
|
|
287
|
+
throw new Error(`No available ports in range ${exports.PORT_RANGE_START}-${exports.PORT_RANGE_END}. Stop other Waymark projects first.`);
|
|
218
288
|
}
|
|
219
289
|
/**
|
|
220
290
|
* Clean up stale entries (processes that are dead but not unregistered)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@way_marks/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Control what AI agents can do in your codebase",
|
|
5
5
|
"author": "Waymark <hello@waymarks.dev>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
]
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@way_marks/server": "0.
|
|
43
|
+
"@way_marks/server": "4.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/jest": "^30.0.0",
|