@way_marks/cli 4.4.2 → 4.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/commands/status.js +16 -0
- package/dist/commands/update.js +56 -0
- package/dist/index.js +10 -0
- package/dist/utils/version-check.js +201 -0
- package/package.json +2 -2
- package/dist/commands/start.test.js +0 -138
package/README.md
CHANGED
|
@@ -139,6 +139,16 @@ npx @way_marks/cli logs --pending # Show only pending actions
|
|
|
139
139
|
npx @way_marks/cli logs --blocked # Show only blocked actions
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
### Version Management
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npx @way_marks/cli --version # Check installed version
|
|
146
|
+
npx @way_marks/cli status # Shows version + update banner if available
|
|
147
|
+
npx @way_marks/cli update # Install latest version from npm
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The CLI automatically checks for updates on startup (non-blocking, 3-second timeout). Results are cached for 24 hours. See the [main README](../../README.md#checking-for-updates) for detailed troubleshooting and update instructions.
|
|
151
|
+
|
|
142
152
|
---
|
|
143
153
|
|
|
144
154
|
## Slack Notifications
|
package/dist/commands/status.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 version_check_1 = require("../utils/version-check");
|
|
39
40
|
async function run() {
|
|
40
41
|
const projectRoot = process.cwd();
|
|
41
42
|
const configPath = path.join(projectRoot, '.waymark', 'config.json');
|
|
@@ -70,6 +71,12 @@ async function run() {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
catch { /* not running */ }
|
|
74
|
+
// Check version (non-blocking)
|
|
75
|
+
let versionInfo = null;
|
|
76
|
+
try {
|
|
77
|
+
versionInfo = await (0, version_check_1.checkVersion)();
|
|
78
|
+
}
|
|
79
|
+
catch { /* version check failed, continue anyway */ }
|
|
73
80
|
console.log('Waymark — Project Status');
|
|
74
81
|
console.log('─'.repeat(35));
|
|
75
82
|
console.log(`Project: ${projectName}`);
|
|
@@ -86,4 +93,13 @@ async function run() {
|
|
|
86
93
|
console.log(`Start with: npx @way_marks/cli start`);
|
|
87
94
|
if (startedAt)
|
|
88
95
|
console.log(`Started: ${startedAt}`);
|
|
96
|
+
// Display version info
|
|
97
|
+
if (versionInfo) {
|
|
98
|
+
console.log('─'.repeat(35));
|
|
99
|
+
console.log(`Version: ${versionInfo.current}`);
|
|
100
|
+
if (versionInfo.updateAvailable && versionInfo.latest) {
|
|
101
|
+
console.log(`⚠️ New version ${versionInfo.latest} available!`);
|
|
102
|
+
console.log(`Update: npm install -g @way_marks/cli@latest`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
89
105
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.run = run;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const version_check_1 = require("../utils/version-check");
|
|
6
|
+
async function run() {
|
|
7
|
+
try {
|
|
8
|
+
const versionInfo = await (0, version_check_1.checkVersion)();
|
|
9
|
+
if (!versionInfo.updateAvailable || !versionInfo.latest) {
|
|
10
|
+
console.log(`✓ Already on latest version ${versionInfo.current}`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log(`Update available: ${versionInfo.current} → ${versionInfo.latest}`);
|
|
14
|
+
console.log('Installing @way_marks/cli@latest...');
|
|
15
|
+
try {
|
|
16
|
+
(0, child_process_1.execSync)('npm install -g @way_marks/cli@latest', {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
timeout: 300000, // 5 minutes
|
|
19
|
+
});
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log('✓ Update complete!');
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log('To start using the updated version:');
|
|
24
|
+
console.log(' • Restart your terminal, or');
|
|
25
|
+
console.log(' • Run: source ~/.bashrc (on Linux/macOS)');
|
|
26
|
+
console.log(' • Run: refreshenv (on Windows PowerShell)');
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error('');
|
|
30
|
+
console.error('✗ npm install failed');
|
|
31
|
+
if (err instanceof Error) {
|
|
32
|
+
console.error(` Error: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
console.error('');
|
|
35
|
+
console.error('Try these troubleshooting steps:');
|
|
36
|
+
console.error(' 1. Check npm is installed and working:');
|
|
37
|
+
console.error(' npm --version');
|
|
38
|
+
console.error(' 2. Try installing again:');
|
|
39
|
+
console.error(' npm install -g @way_marks/cli@latest');
|
|
40
|
+
console.error(' 3. Check npm permissions:');
|
|
41
|
+
console.error(' npm config get prefix');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error('✗ Version check failed');
|
|
47
|
+
if (err instanceof Error) {
|
|
48
|
+
console.error(` Error: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
console.error('');
|
|
51
|
+
console.error('Unable to check for updates. This may be a network issue.');
|
|
52
|
+
console.error('You can manually update with:');
|
|
53
|
+
console.error(' npm install -g @way_marks/cli@latest');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const version_check_1 = require("./utils/version-check");
|
|
3
5
|
function getVersion() {
|
|
4
6
|
// dist/index.js → ../package.json (when published or installed globally)
|
|
5
7
|
// dev (ts-node from src/) → ../package.json relative to repo
|
|
@@ -28,6 +30,7 @@ function printHelp() {
|
|
|
28
30
|
console.log(' pause Pause a project (keep port allocated)');
|
|
29
31
|
console.log(' resume Resume a paused project');
|
|
30
32
|
console.log(' status Show current Waymark status and pending count');
|
|
33
|
+
console.log(' update Check for and install the latest version');
|
|
31
34
|
console.log(' logs Show recent action log');
|
|
32
35
|
console.log(' agents List running AI agent sessions');
|
|
33
36
|
console.log(' list List all registered Waymark projects');
|
|
@@ -53,6 +56,10 @@ if (command === '-h' || command === '--help' || command === 'help' || command ==
|
|
|
53
56
|
printHelp();
|
|
54
57
|
process.exit(command === undefined ? 0 : 0);
|
|
55
58
|
}
|
|
59
|
+
// Fire off version check asynchronously (non-blocking)
|
|
60
|
+
(0, version_check_1.runVersionCheckAsync)(1000).catch(() => {
|
|
61
|
+
// Silently ignore any errors from version check
|
|
62
|
+
});
|
|
56
63
|
switch (command) {
|
|
57
64
|
case 'init':
|
|
58
65
|
require('./commands/init').run();
|
|
@@ -72,6 +79,9 @@ switch (command) {
|
|
|
72
79
|
case 'status':
|
|
73
80
|
require('./commands/status').run();
|
|
74
81
|
break;
|
|
82
|
+
case 'update':
|
|
83
|
+
require('./commands/update').run();
|
|
84
|
+
break;
|
|
75
85
|
case 'logs':
|
|
76
86
|
require('./commands/logs').run();
|
|
77
87
|
break;
|
|
@@ -0,0 +1,201 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.checkVersion = checkVersion;
|
|
37
|
+
exports.shouldSkipVersionCheck = shouldSkipVersionCheck;
|
|
38
|
+
exports.printUpdateBanner = printUpdateBanner;
|
|
39
|
+
exports.runVersionCheckAsync = runVersionCheckAsync;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
43
|
+
function getCacheFilePath() {
|
|
44
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
45
|
+
return path.join(homeDir, '.waymark', 'cli-version-cache.json');
|
|
46
|
+
}
|
|
47
|
+
function readCache() {
|
|
48
|
+
try {
|
|
49
|
+
const cacheFile = getCacheFilePath();
|
|
50
|
+
if (!fs.existsSync(cacheFile))
|
|
51
|
+
return null;
|
|
52
|
+
const data = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
53
|
+
const age = Date.now() - data.checkedAt;
|
|
54
|
+
if (age > CACHE_TTL_MS)
|
|
55
|
+
return null;
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function writeCache(data) {
|
|
63
|
+
try {
|
|
64
|
+
const cacheDir = path.dirname(getCacheFilePath());
|
|
65
|
+
if (!fs.existsSync(cacheDir)) {
|
|
66
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
fs.writeFileSync(getCacheFilePath(), JSON.stringify(data), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Silently fail if we can't write cache
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function getCurrentVersion() {
|
|
75
|
+
try {
|
|
76
|
+
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
77
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
78
|
+
return pkg.version;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return 'unknown';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function fetchLatestVersion() {
|
|
85
|
+
try {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
88
|
+
const res = await fetch('https://registry.npmjs.org/@way_marks/cli/latest', {
|
|
89
|
+
signal: controller.signal,
|
|
90
|
+
});
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
if (!res.ok)
|
|
93
|
+
return null;
|
|
94
|
+
const data = await res.json();
|
|
95
|
+
return data.version || null;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function compareVersions(current, latest) {
|
|
102
|
+
try {
|
|
103
|
+
const parseCurrent = current.split('.').map(Number);
|
|
104
|
+
const parseLatest = latest.split('.').map(Number);
|
|
105
|
+
for (let i = 0; i < 3; i++) {
|
|
106
|
+
const curr = parseCurrent[i] || 0;
|
|
107
|
+
const latestVal = parseLatest[i] || 0;
|
|
108
|
+
if (latestVal > curr)
|
|
109
|
+
return true;
|
|
110
|
+
if (curr > latestVal)
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function checkVersion() {
|
|
120
|
+
const current = getCurrentVersion();
|
|
121
|
+
// Try to use cache first
|
|
122
|
+
const cached = readCache();
|
|
123
|
+
if (cached && cached.current === current) {
|
|
124
|
+
return {
|
|
125
|
+
current: cached.current,
|
|
126
|
+
latest: cached.latest,
|
|
127
|
+
updateAvailable: compareVersions(cached.current, cached.latest || ''),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Fetch latest version
|
|
131
|
+
const latest = await fetchLatestVersion();
|
|
132
|
+
// Write to cache
|
|
133
|
+
if (latest) {
|
|
134
|
+
writeCache({
|
|
135
|
+
current,
|
|
136
|
+
latest,
|
|
137
|
+
checkedAt: Date.now(),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
current,
|
|
142
|
+
latest,
|
|
143
|
+
updateAvailable: latest ? compareVersions(current, latest) : false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function shouldSkipVersionCheck() {
|
|
147
|
+
// Check environment variable
|
|
148
|
+
if (process.env.WAYMARK_SKIP_VERSION_CHECK === '1') {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
// Check ~/.waymark/config.json
|
|
152
|
+
try {
|
|
153
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
154
|
+
const configPath = path.join(homeDir, '.waymark', 'config.json');
|
|
155
|
+
if (fs.existsSync(configPath)) {
|
|
156
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
157
|
+
if (config.skipVersionCheck === true) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Ignore errors reading config
|
|
164
|
+
}
|
|
165
|
+
// Check .waymarkrc in cwd
|
|
166
|
+
try {
|
|
167
|
+
const rcPath = path.join(process.cwd(), '.waymarkrc');
|
|
168
|
+
if (fs.existsSync(rcPath)) {
|
|
169
|
+
const config = JSON.parse(fs.readFileSync(rcPath, 'utf8'));
|
|
170
|
+
if (config.skipVersionCheck === true) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Ignore errors reading .waymarkrc
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
function printUpdateBanner(current, latest) {
|
|
181
|
+
const banner = `⚠️ Waymark ${current} → ${latest} available! Run: npx @way_marks/cli update`;
|
|
182
|
+
console.error(banner);
|
|
183
|
+
}
|
|
184
|
+
async function runVersionCheckAsync(timeoutMs = 1000) {
|
|
185
|
+
if (shouldSkipVersionCheck()) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
191
|
+
// Create a promise that checks the version
|
|
192
|
+
const checkPromise = checkVersion().finally(() => clearTimeout(timeoutId));
|
|
193
|
+
const result = await checkPromise;
|
|
194
|
+
if (result.updateAvailable && result.latest) {
|
|
195
|
+
printUpdateBanner(result.current, result.latest);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Silently fail on any error (including timeout)
|
|
200
|
+
}
|
|
201
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@way_marks/cli",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.4",
|
|
4
4
|
"description": "Control what AI agents can do in your codebase",
|
|
5
5
|
"author": "Waymark <hello@waymarks.dev>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@way_marks/server": "4.4.
|
|
46
|
+
"@way_marks/server": "4.4.4"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/jest": "^30.0.0",
|
|
@@ -1,138 +0,0 @@
|
|
|
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
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
const net = __importStar(require("net"));
|
|
37
|
-
// ─── kebabCase ────────────────────────────────────────────────────────────────
|
|
38
|
-
// We inline kebabCase here because it is not exported from start.ts.
|
|
39
|
-
// This matches the implementation exactly.
|
|
40
|
-
function kebabCase(str) {
|
|
41
|
-
return str
|
|
42
|
-
.toLowerCase()
|
|
43
|
-
.replace(/[\s_]+/g, '-')
|
|
44
|
-
.replace(/[^a-z0-9-]/g, '')
|
|
45
|
-
.replace(/-+/g, '-')
|
|
46
|
-
.replace(/^-|-$/g, '');
|
|
47
|
-
}
|
|
48
|
-
describe('kebabCase', () => {
|
|
49
|
-
it('lowercases the string', () => {
|
|
50
|
-
expect(kebabCase('MyProject')).toBe('myproject');
|
|
51
|
-
});
|
|
52
|
-
it('replaces spaces with dashes', () => {
|
|
53
|
-
expect(kebabCase('my project')).toBe('my-project');
|
|
54
|
-
});
|
|
55
|
-
it('replaces underscores with dashes', () => {
|
|
56
|
-
expect(kebabCase('my_project')).toBe('my-project');
|
|
57
|
-
});
|
|
58
|
-
it('replaces multiple spaces/underscores with a single dash', () => {
|
|
59
|
-
expect(kebabCase('my __project')).toBe('my-project');
|
|
60
|
-
});
|
|
61
|
-
it('strips special characters', () => {
|
|
62
|
-
expect(kebabCase('my@project!')).toBe('myproject');
|
|
63
|
-
});
|
|
64
|
-
it('collapses multiple dashes', () => {
|
|
65
|
-
expect(kebabCase('my---project')).toBe('my-project');
|
|
66
|
-
});
|
|
67
|
-
it('strips leading dashes', () => {
|
|
68
|
-
expect(kebabCase('-my-project')).toBe('my-project');
|
|
69
|
-
});
|
|
70
|
-
it('strips trailing dashes', () => {
|
|
71
|
-
expect(kebabCase('my-project-')).toBe('my-project');
|
|
72
|
-
});
|
|
73
|
-
it('handles already clean kebab input', () => {
|
|
74
|
-
expect(kebabCase('my-project')).toBe('my-project');
|
|
75
|
-
});
|
|
76
|
-
it('handles numbers in string', () => {
|
|
77
|
-
expect(kebabCase('project-v2-api')).toBe('project-v2-api');
|
|
78
|
-
});
|
|
79
|
-
it('handles all-special-char input returning empty string', () => {
|
|
80
|
-
expect(kebabCase('!!!@@@')).toBe('');
|
|
81
|
-
});
|
|
82
|
-
it('handles typical project directory names', () => {
|
|
83
|
-
expect(kebabCase('ecommerce_backend')).toBe('ecommerce-backend');
|
|
84
|
-
expect(kebabCase('Japan Travel App')).toBe('japan-travel-app');
|
|
85
|
-
expect(kebabCase('my.project.v2')).toBe('myprojectv2');
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
// ─── findAvailablePort ────────────────────────────────────────────────────────
|
|
89
|
-
function findAvailablePort(preferred) {
|
|
90
|
-
return new Promise((resolve) => {
|
|
91
|
-
const server = net.createServer();
|
|
92
|
-
server.listen(preferred, () => {
|
|
93
|
-
const port = server.address().port;
|
|
94
|
-
server.close(() => resolve(port));
|
|
95
|
-
});
|
|
96
|
-
server.on('error', () => {
|
|
97
|
-
// No ceiling in tests — recurse to next port unconditionally
|
|
98
|
-
resolve(findAvailablePort(preferred + 1));
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
/** Find a free high port dynamically so tests don't depend on 3001 being free */
|
|
103
|
-
function getFreePort() {
|
|
104
|
-
return new Promise((resolve) => {
|
|
105
|
-
const srv = net.createServer();
|
|
106
|
-
srv.listen(0, () => {
|
|
107
|
-
const port = srv.address().port;
|
|
108
|
-
srv.close(() => resolve(port));
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
describe('findAvailablePort', () => {
|
|
113
|
-
it('returns the preferred port when it is free', async () => {
|
|
114
|
-
const free = await getFreePort();
|
|
115
|
-
const port = await findAvailablePort(free);
|
|
116
|
-
expect(port).toBe(free);
|
|
117
|
-
});
|
|
118
|
-
it('falls back to next port when preferred is in use', async () => {
|
|
119
|
-
const base = await getFreePort();
|
|
120
|
-
// Occupy base
|
|
121
|
-
const blocker = net.createServer();
|
|
122
|
-
await new Promise(res => blocker.listen(base, res));
|
|
123
|
-
try {
|
|
124
|
-
const port = await findAvailablePort(base);
|
|
125
|
-
expect(port).not.toBe(base);
|
|
126
|
-
expect(port).toBeGreaterThan(0);
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
await new Promise(res => blocker.close(() => res()));
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
it('returns a number (valid port)', async () => {
|
|
133
|
-
const free = await getFreePort();
|
|
134
|
-
const port = await findAvailablePort(free);
|
|
135
|
-
expect(typeof port).toBe('number');
|
|
136
|
-
expect(port).toBeGreaterThan(0);
|
|
137
|
-
});
|
|
138
|
-
});
|