@mondaydotcomorg/z2h-cli 0.23.1 → 0.24.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/commands/__tests__/generate-z2h-token.test.d.ts +2 -0
- package/dist/commands/__tests__/generate-z2h-token.test.d.ts.map +1 -0
- package/dist/commands/__tests__/migrate.test.d.ts +2 -0
- package/dist/commands/__tests__/migrate.test.d.ts.map +1 -0
- package/dist/commands/generate-z2h-token.d.ts.map +1 -1
- package/dist/commands/generate-z2h-token.js +17 -4
- package/dist/commands/migrate.d.ts +1 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +17 -6
- package/dist/commands/tag.d.ts.map +1 -1
- package/dist/commands/tag.js +3 -0
- package/dist/esm/commands/__tests__/generate-z2h-token.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/generate-z2h-token.test.d.ts.map +1 -0
- package/dist/esm/commands/__tests__/migrate.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/migrate.test.d.ts.map +1 -0
- package/dist/esm/commands/generate-z2h-token.d.ts.map +1 -1
- package/dist/esm/commands/generate-z2h-token.mjs +17 -4
- package/dist/esm/commands/migrate.d.ts +1 -0
- package/dist/esm/commands/migrate.d.ts.map +1 -1
- package/dist/esm/commands/migrate.mjs +17 -7
- package/dist/esm/commands/tag.d.ts.map +1 -1
- package/dist/esm/commands/tag.mjs +3 -0
- package/package.json +1 -1
- package/src/commands/__tests__/generate-z2h-token.test.ts +140 -0
- package/src/commands/__tests__/migrate.test.ts +34 -0
- package/src/commands/generate-z2h-token.ts +19 -3
- package/src/commands/migrate.ts +17 -6
- package/src/commands/tag.ts +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-z2h-token.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/generate-z2h-token.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/migrate.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-z2h-token.d.ts","sourceRoot":"","sources":["../../src/commands/generate-z2h-token.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,yDAAyD,CAAC;AAC7F,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;
|
|
1
|
+
{"version":3,"file":"generate-z2h-token.d.ts","sourceRoot":"","sources":["../../src/commands/generate-z2h-token.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,yDAAyD,CAAC;AAC7F,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAwED,wBAAsB,uBAAuB,CAAC,IAAI,GAAE,uBAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B/F"}
|
|
@@ -15,6 +15,9 @@ const DEFAULT_TOKEN_PAGE_URL = 'https://bigbrain.me/bigbrain-vibe/generate-z2h-t
|
|
|
15
15
|
const LOOPBACK_TIMEOUT_MS = 120_000;
|
|
16
16
|
async function promptForToken() {
|
|
17
17
|
const rl = readline__default.default.createInterface({ input: node_process.stdin, output: node_process.stdout });
|
|
18
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
19
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
20
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
18
21
|
try {
|
|
19
22
|
const raw = await rl.question('Paste your token here, then press Enter:\n> ');
|
|
20
23
|
return raw.trim();
|
|
@@ -51,13 +54,23 @@ async function runLoopbackFlow(pageUrl) {
|
|
|
51
54
|
util_logger.warn('could not auto-open the browser. please visit the URL above manually.');
|
|
52
55
|
}
|
|
53
56
|
util_logger.info('waiting for the page to send the token (Ctrl+C to fall back to manual paste)…');
|
|
57
|
+
// A SIGINT listener suppresses Node's default termination while it is attached. That is
|
|
58
|
+
// what turns the Ctrl+C offered above into the manual-paste fallback instead of a killed
|
|
59
|
+
// CLI. The finally below detaches it, so it covers only this wait.
|
|
60
|
+
let stopListening = () => { };
|
|
61
|
+
const cancelled = new Promise(resolve => {
|
|
62
|
+
const onSigint = () => resolve(null);
|
|
63
|
+
process.once('SIGINT', onSigint);
|
|
64
|
+
stopListening = () => process.removeListener('SIGINT', onSigint);
|
|
65
|
+
});
|
|
66
|
+
// `close()` below rejects a still-pending wait. Swallow it here so the loser of the race
|
|
67
|
+
// never becomes an unhandled rejection, which Node turns into a crash.
|
|
68
|
+
const arrived = loopback.waitForToken(LOOPBACK_TIMEOUT_MS).catch(() => null);
|
|
54
69
|
try {
|
|
55
|
-
return await
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return null;
|
|
70
|
+
return await Promise.race([arrived, cancelled]);
|
|
59
71
|
}
|
|
60
72
|
finally {
|
|
73
|
+
stopListening();
|
|
61
74
|
loopback.close();
|
|
62
75
|
}
|
|
63
76
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AA4BA,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMnD;AAqBD,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmGjG"}
|
package/dist/commands/migrate.js
CHANGED
|
@@ -16,6 +16,15 @@ const semver__default = /*#__PURE__*/_interopDefault(semver);
|
|
|
16
16
|
|
|
17
17
|
const STATE_FILE = '.z2h-migrations-state.json';
|
|
18
18
|
// ─── Helpers ───
|
|
19
|
+
// A prerelease sorts BELOW its own release under plain semver ordering, so
|
|
20
|
+
// comparing raw beta versions here would skip migrations the beta introduced.
|
|
21
|
+
function baseVersion(version) {
|
|
22
|
+
const parsed = semver__default.default.parse(version);
|
|
23
|
+
if (!parsed) {
|
|
24
|
+
throw new Error(`invalid semver version: ${version}`);
|
|
25
|
+
}
|
|
26
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
27
|
+
}
|
|
19
28
|
async function readMigrationState(workspaceHome) {
|
|
20
29
|
const statePath = path__default.default.join(workspaceHome, STATE_FILE);
|
|
21
30
|
if (!(await fs.pathExists(statePath))) {
|
|
@@ -40,11 +49,12 @@ async function migrateCommand(cliVersion, opts = {}) {
|
|
|
40
49
|
}
|
|
41
50
|
const state = await readMigrationState(workspaceHome);
|
|
42
51
|
const lastVersion = state?.lastMigratedVersion ?? '0.0.0';
|
|
43
|
-
|
|
52
|
+
const targetVersion = baseVersion(cliVersion);
|
|
53
|
+
if (!semver__default.default.gt(targetVersion, lastVersion)) {
|
|
44
54
|
util_logger.info('workspace is up to date — no migrations pending');
|
|
45
55
|
return;
|
|
46
56
|
}
|
|
47
|
-
util_logger.info(`migrating workspace from v${lastVersion} → v${
|
|
57
|
+
util_logger.info(`migrating workspace from v${lastVersion} → v${targetVersion}...`);
|
|
48
58
|
// migrations.json is at the package root — two dirs up from dist/migrations/
|
|
49
59
|
const thisDir = path__default.default.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('commands/migrate.js', document.baseURI).href))));
|
|
50
60
|
const migrationsJsonPath = path__default.default.resolve(thisDir, '..', '..', 'migrations.json');
|
|
@@ -53,12 +63,12 @@ async function migrateCommand(cliVersion, opts = {}) {
|
|
|
53
63
|
return;
|
|
54
64
|
}
|
|
55
65
|
const migrationsJson = (await fs.readJson(migrationsJsonPath));
|
|
56
|
-
// Filter to pending migrations (version > lastVersion AND <=
|
|
66
|
+
// Filter to pending migrations (version > lastVersion AND <= targetVersion).
|
|
57
67
|
// CONVENTION: a migration's `version` field must equal the CLI release it
|
|
58
68
|
// ships with (not the feature branch version). This ensures the `gt` check
|
|
59
69
|
// always catches a migration on the exact upgrade that introduces it.
|
|
60
70
|
const pending = Object.entries(migrationsJson.migrations)
|
|
61
|
-
.filter(([, entry]) => semver__default.default.gt(entry.version, lastVersion) && semver__default.default.lte(entry.version,
|
|
71
|
+
.filter(([, entry]) => semver__default.default.gt(entry.version, lastVersion) && semver__default.default.lte(entry.version, targetVersion))
|
|
62
72
|
.sort(([, a], [, b]) => semver__default.default.compare(a.version, b.version));
|
|
63
73
|
if (opts.dryRun) {
|
|
64
74
|
util_logger.info(`${pending.length} pending migration(s) (dry-run — no changes will be applied):`);
|
|
@@ -68,7 +78,7 @@ async function migrateCommand(cliVersion, opts = {}) {
|
|
|
68
78
|
return;
|
|
69
79
|
}
|
|
70
80
|
if (pending.length === 0) {
|
|
71
|
-
await writeMigrationState(workspaceHome, { lastMigratedVersion:
|
|
81
|
+
await writeMigrationState(workspaceHome, { lastMigratedVersion: targetVersion });
|
|
72
82
|
return;
|
|
73
83
|
}
|
|
74
84
|
const nxReady = await util_workspace.ensureWorkspaceNx(workspaceHome);
|
|
@@ -108,8 +118,9 @@ async function migrateCommand(cliVersion, opts = {}) {
|
|
|
108
118
|
if (needsInstall) {
|
|
109
119
|
await util_workspace.runYarnInstall(workspaceHome, 'package.json changed');
|
|
110
120
|
}
|
|
111
|
-
await writeMigrationState(workspaceHome, { lastMigratedVersion:
|
|
121
|
+
await writeMigrationState(workspaceHome, { lastMigratedVersion: targetVersion });
|
|
112
122
|
util_logger.info('workspace migrations complete.');
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
exports.baseVersion = baseVersion;
|
|
115
126
|
exports.migrateCommand = migrateCommand;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA6ClH;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B5F"}
|
package/dist/commands/tag.js
CHANGED
|
@@ -46,6 +46,9 @@ async function resolveAppSlug(appArg) {
|
|
|
46
46
|
*/
|
|
47
47
|
async function promptForTags(appSlug, catalog, current) {
|
|
48
48
|
const rl = readline__default.default.createInterface({ input: node_process.stdin, output: node_process.stdout });
|
|
49
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
50
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
51
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
49
52
|
const selected = [];
|
|
50
53
|
try {
|
|
51
54
|
util_logger.info(`Tagging "${appSlug}".`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-z2h-token.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/__tests__/generate-z2h-token.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/__tests__/migrate.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-z2h-token.d.ts","sourceRoot":"","sources":["../../../src/commands/generate-z2h-token.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,yDAAyD,CAAC;AAC7F,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;
|
|
1
|
+
{"version":3,"file":"generate-z2h-token.d.ts","sourceRoot":"","sources":["../../../src/commands/generate-z2h-token.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,yDAAyD,CAAC;AAC7F,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAwED,wBAAsB,uBAAuB,CAAC,IAAI,GAAE,uBAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B/F"}
|
|
@@ -9,6 +9,9 @@ const DEFAULT_TOKEN_PAGE_URL = 'https://bigbrain.me/bigbrain-vibe/generate-z2h-t
|
|
|
9
9
|
const LOOPBACK_TIMEOUT_MS = 120_000;
|
|
10
10
|
async function promptForToken() {
|
|
11
11
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
12
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
13
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
14
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
12
15
|
try {
|
|
13
16
|
const raw = await rl.question('Paste your token here, then press Enter:\n> ');
|
|
14
17
|
return raw.trim();
|
|
@@ -45,13 +48,23 @@ async function runLoopbackFlow(pageUrl) {
|
|
|
45
48
|
warn('could not auto-open the browser. please visit the URL above manually.');
|
|
46
49
|
}
|
|
47
50
|
info('waiting for the page to send the token (Ctrl+C to fall back to manual paste)…');
|
|
51
|
+
// A SIGINT listener suppresses Node's default termination while it is attached. That is
|
|
52
|
+
// what turns the Ctrl+C offered above into the manual-paste fallback instead of a killed
|
|
53
|
+
// CLI. The finally below detaches it, so it covers only this wait.
|
|
54
|
+
let stopListening = () => { };
|
|
55
|
+
const cancelled = new Promise(resolve => {
|
|
56
|
+
const onSigint = () => resolve(null);
|
|
57
|
+
process.once('SIGINT', onSigint);
|
|
58
|
+
stopListening = () => process.removeListener('SIGINT', onSigint);
|
|
59
|
+
});
|
|
60
|
+
// `close()` below rejects a still-pending wait. Swallow it here so the loser of the race
|
|
61
|
+
// never becomes an unhandled rejection, which Node turns into a crash.
|
|
62
|
+
const arrived = loopback.waitForToken(LOOPBACK_TIMEOUT_MS).catch(() => null);
|
|
48
63
|
try {
|
|
49
|
-
return await
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return null;
|
|
64
|
+
return await Promise.race([arrived, cancelled]);
|
|
53
65
|
}
|
|
54
66
|
finally {
|
|
67
|
+
stopListening();
|
|
55
68
|
loopback.close();
|
|
56
69
|
}
|
|
57
70
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/commands/migrate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/commands/migrate.ts"],"names":[],"mappings":"AA4BA,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMnD;AAqBD,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmGjG"}
|
|
@@ -8,6 +8,15 @@ import { getWorkspaceHome, isZ2hWorkspace, ensureWorkspaceNx, runYarnInstall } f
|
|
|
8
8
|
|
|
9
9
|
const STATE_FILE = '.z2h-migrations-state.json';
|
|
10
10
|
// ─── Helpers ───
|
|
11
|
+
// A prerelease sorts BELOW its own release under plain semver ordering, so
|
|
12
|
+
// comparing raw beta versions here would skip migrations the beta introduced.
|
|
13
|
+
function baseVersion(version) {
|
|
14
|
+
const parsed = semver.parse(version);
|
|
15
|
+
if (!parsed) {
|
|
16
|
+
throw new Error(`invalid semver version: ${version}`);
|
|
17
|
+
}
|
|
18
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
19
|
+
}
|
|
11
20
|
async function readMigrationState(workspaceHome) {
|
|
12
21
|
const statePath = path.join(workspaceHome, STATE_FILE);
|
|
13
22
|
if (!(await pathExists(statePath))) {
|
|
@@ -32,11 +41,12 @@ async function migrateCommand(cliVersion, opts = {}) {
|
|
|
32
41
|
}
|
|
33
42
|
const state = await readMigrationState(workspaceHome);
|
|
34
43
|
const lastVersion = state?.lastMigratedVersion ?? '0.0.0';
|
|
35
|
-
|
|
44
|
+
const targetVersion = baseVersion(cliVersion);
|
|
45
|
+
if (!semver.gt(targetVersion, lastVersion)) {
|
|
36
46
|
info('workspace is up to date — no migrations pending');
|
|
37
47
|
return;
|
|
38
48
|
}
|
|
39
|
-
info(`migrating workspace from v${lastVersion} → v${
|
|
49
|
+
info(`migrating workspace from v${lastVersion} → v${targetVersion}...`);
|
|
40
50
|
// migrations.json is at the package root — two dirs up from dist/migrations/
|
|
41
51
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
42
52
|
const migrationsJsonPath = path.resolve(thisDir, '..', '..', 'migrations.json');
|
|
@@ -45,12 +55,12 @@ async function migrateCommand(cliVersion, opts = {}) {
|
|
|
45
55
|
return;
|
|
46
56
|
}
|
|
47
57
|
const migrationsJson = (await readJson(migrationsJsonPath));
|
|
48
|
-
// Filter to pending migrations (version > lastVersion AND <=
|
|
58
|
+
// Filter to pending migrations (version > lastVersion AND <= targetVersion).
|
|
49
59
|
// CONVENTION: a migration's `version` field must equal the CLI release it
|
|
50
60
|
// ships with (not the feature branch version). This ensures the `gt` check
|
|
51
61
|
// always catches a migration on the exact upgrade that introduces it.
|
|
52
62
|
const pending = Object.entries(migrationsJson.migrations)
|
|
53
|
-
.filter(([, entry]) => semver.gt(entry.version, lastVersion) && semver.lte(entry.version,
|
|
63
|
+
.filter(([, entry]) => semver.gt(entry.version, lastVersion) && semver.lte(entry.version, targetVersion))
|
|
54
64
|
.sort(([, a], [, b]) => semver.compare(a.version, b.version));
|
|
55
65
|
if (opts.dryRun) {
|
|
56
66
|
info(`${pending.length} pending migration(s) (dry-run — no changes will be applied):`);
|
|
@@ -60,7 +70,7 @@ async function migrateCommand(cliVersion, opts = {}) {
|
|
|
60
70
|
return;
|
|
61
71
|
}
|
|
62
72
|
if (pending.length === 0) {
|
|
63
|
-
await writeMigrationState(workspaceHome, { lastMigratedVersion:
|
|
73
|
+
await writeMigrationState(workspaceHome, { lastMigratedVersion: targetVersion });
|
|
64
74
|
return;
|
|
65
75
|
}
|
|
66
76
|
const nxReady = await ensureWorkspaceNx(workspaceHome);
|
|
@@ -100,8 +110,8 @@ async function migrateCommand(cliVersion, opts = {}) {
|
|
|
100
110
|
if (needsInstall) {
|
|
101
111
|
await runYarnInstall(workspaceHome, 'package.json changed');
|
|
102
112
|
}
|
|
103
|
-
await writeMigrationState(workspaceHome, { lastMigratedVersion:
|
|
113
|
+
await writeMigrationState(workspaceHome, { lastMigratedVersion: targetVersion });
|
|
104
114
|
info('workspace migrations complete.');
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
export { migrateCommand };
|
|
117
|
+
export { baseVersion, migrateCommand };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA6ClH;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B5F"}
|
|
@@ -40,6 +40,9 @@ async function resolveAppSlug(appArg) {
|
|
|
40
40
|
*/
|
|
41
41
|
async function promptForTags(appSlug, catalog, current) {
|
|
42
42
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
43
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
44
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
45
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
43
46
|
const selected = [];
|
|
44
47
|
try {
|
|
45
48
|
info(`Tagging "${appSlug}".`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { generateZ2HTokenCommand } from '../generate-z2h-token';
|
|
5
|
+
import { startLoopbackServer } from '../../util/auth/loopback-server';
|
|
6
|
+
import { writeZthAuth } from '../../util/auth/write-auth';
|
|
7
|
+
import { openInBrowser } from '../../util/open-browser';
|
|
8
|
+
import { setOutputMode } from '../../util/logger';
|
|
9
|
+
|
|
10
|
+
vi.mock('node:readline/promises', () => ({
|
|
11
|
+
default: { createInterface: vi.fn() },
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('../../util/auth/loopback-server', () => ({ startLoopbackServer: vi.fn() }));
|
|
14
|
+
vi.mock('../../util/auth/write-auth', async importOriginal => ({
|
|
15
|
+
...(await importOriginal<typeof import('../../util/auth/write-auth')>()),
|
|
16
|
+
writeZthAuth: vi.fn().mockResolvedValue({ filePath: '/tmp/auth.json' }),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('../../util/open-browser', () => ({ openInBrowser: vi.fn().mockResolvedValue(undefined) }));
|
|
19
|
+
|
|
20
|
+
const PASTED_TOKEN = 'aaa.bbb.ccc';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mirrors the real LoopbackHandle contract: `close()` rejects a wait that never
|
|
24
|
+
* received a token. A race that leaves that rejection unhandled crashes the process
|
|
25
|
+
* under Node's default unhandled-rejection policy, so the fake must reproduce it.
|
|
26
|
+
*/
|
|
27
|
+
function fakeLoopback() {
|
|
28
|
+
let rejectPending: ((err: Error) => void) | null = null;
|
|
29
|
+
const close = vi.fn(() => {
|
|
30
|
+
rejectPending?.(new Error('Loopback server closed before token arrived'));
|
|
31
|
+
rejectPending = null;
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
callbackUrl: 'http://127.0.0.1:1234/token',
|
|
35
|
+
waitForToken: vi.fn(
|
|
36
|
+
() =>
|
|
37
|
+
new Promise<string>((_res, rej) => {
|
|
38
|
+
rejectPending = rej;
|
|
39
|
+
})
|
|
40
|
+
),
|
|
41
|
+
close,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('generate-z2h-token: Ctrl+C falls back to manual paste', () => {
|
|
46
|
+
let question: ReturnType<typeof vi.fn>;
|
|
47
|
+
let rlClose: ReturnType<typeof vi.fn>;
|
|
48
|
+
let rlOn: ReturnType<typeof vi.fn>;
|
|
49
|
+
const unhandled: unknown[] = [];
|
|
50
|
+
const onUnhandled = (err: unknown) => unhandled.push(err);
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
unhandled.length = 0;
|
|
55
|
+
process.on('unhandledRejection', onUnhandled);
|
|
56
|
+
setOutputMode('json');
|
|
57
|
+
question = vi.fn().mockResolvedValue(PASTED_TOKEN);
|
|
58
|
+
rlClose = vi.fn();
|
|
59
|
+
rlOn = vi.fn();
|
|
60
|
+
vi.mocked(readline.createInterface).mockReturnValue({
|
|
61
|
+
question,
|
|
62
|
+
close: rlClose,
|
|
63
|
+
on: rlOn,
|
|
64
|
+
} as unknown as ReturnType<typeof readline.createInterface>);
|
|
65
|
+
vi.mocked(startLoopbackServer).mockResolvedValue(fakeLoopback());
|
|
66
|
+
vi.mocked(writeZthAuth).mockResolvedValue({ filePath: '/tmp/auth.json' });
|
|
67
|
+
vi.mocked(openInBrowser).mockResolvedValue(undefined);
|
|
68
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
process.removeListener('unhandledRejection', onUnhandled);
|
|
73
|
+
setOutputMode('human');
|
|
74
|
+
vi.mocked(process.stdout.write).mockRestore();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('cancels the loopback wait on SIGINT and saves the pasted token instead', async () => {
|
|
78
|
+
const done = generateZ2HTokenCommand();
|
|
79
|
+
// Let runLoopbackFlow reach its wait and attach the listener before interrupting.
|
|
80
|
+
await vi.waitFor(() => expect(process.listenerCount('SIGINT')).toBeGreaterThan(0));
|
|
81
|
+
process.emit('SIGINT');
|
|
82
|
+
|
|
83
|
+
await done;
|
|
84
|
+
|
|
85
|
+
expect(question).toHaveBeenCalledOnce();
|
|
86
|
+
expect(writeZthAuth).toHaveBeenCalledWith({ token: PASTED_TOKEN });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('detaches its SIGINT listener so a later Ctrl+C is not swallowed', async () => {
|
|
90
|
+
const before = process.listenerCount('SIGINT');
|
|
91
|
+
const done = generateZ2HTokenCommand();
|
|
92
|
+
await vi.waitFor(() => expect(process.listenerCount('SIGINT')).toBe(before + 1));
|
|
93
|
+
process.emit('SIGINT');
|
|
94
|
+
await done;
|
|
95
|
+
|
|
96
|
+
expect(process.listenerCount('SIGINT')).toBe(before);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('never leaves the losing race branch as an unhandled rejection', async () => {
|
|
100
|
+
const done = generateZ2HTokenCommand();
|
|
101
|
+
await vi.waitFor(() => expect(process.listenerCount('SIGINT')).toBeGreaterThan(0));
|
|
102
|
+
process.emit('SIGINT');
|
|
103
|
+
await done;
|
|
104
|
+
// close() rejected the pending wait; that rejection must already be swallowed.
|
|
105
|
+
await new Promise(r => setImmediate(r));
|
|
106
|
+
|
|
107
|
+
expect(unhandled).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('still uses the loopback token when one arrives before any Ctrl+C', async () => {
|
|
111
|
+
const handle = fakeLoopback();
|
|
112
|
+
handle.waitForToken.mockResolvedValue('xxx.yyy.zzz');
|
|
113
|
+
vi.mocked(startLoopbackServer).mockResolvedValue(handle);
|
|
114
|
+
|
|
115
|
+
await generateZ2HTokenCommand();
|
|
116
|
+
|
|
117
|
+
expect(question).not.toHaveBeenCalled();
|
|
118
|
+
expect(writeZthAuth).toHaveBeenCalledWith({ token: 'xxx.yyy.zzz' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('--no-browser skips the loopback entirely and prompts straight away', async () => {
|
|
122
|
+
await generateZ2HTokenCommand({ noBrowser: true });
|
|
123
|
+
|
|
124
|
+
expect(startLoopbackServer).not.toHaveBeenCalled();
|
|
125
|
+
expect(openInBrowser).not.toHaveBeenCalled();
|
|
126
|
+
expect(question).toHaveBeenCalledOnce();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Raw mode swallows Ctrl-C unless the interface itself listens, so the prompt must
|
|
130
|
+
// register a handler or the CLI hangs there with no way out.
|
|
131
|
+
it('makes Ctrl-C exit the paste prompt instead of pausing it', async () => {
|
|
132
|
+
await generateZ2HTokenCommand({ noBrowser: true });
|
|
133
|
+
|
|
134
|
+
expect(rlOn).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
|
135
|
+
const exit = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);
|
|
136
|
+
rlOn.mock.calls.find(([event]) => event === 'SIGINT')?.[1]();
|
|
137
|
+
expect(exit).toHaveBeenCalledWith(130);
|
|
138
|
+
exit.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import { baseVersion } from '../migrate';
|
|
4
|
+
|
|
5
|
+
describe('baseVersion', () => {
|
|
6
|
+
it('strips prerelease + build metadata down to major.minor.patch', () => {
|
|
7
|
+
expect(baseVersion('0.23.0-feat-faster-deploy-abc1234')).toBe('0.23.0');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('is a no-op on an already-plain release version', () => {
|
|
11
|
+
expect(baseVersion('0.23.0')).toBe('0.23.0');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('throws on an invalid version string rather than silently misbehaving', () => {
|
|
15
|
+
expect(() => baseVersion('not-a-version')).toThrow(/invalid semver/);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('prerelease migration gating (regression for the bug baseVersion fixes)', () => {
|
|
20
|
+
const MIGRATION_VERSION = '0.23.0';
|
|
21
|
+
const BETA_CLI_VERSION = '0.23.0-feat-faster-deploy-abc1234';
|
|
22
|
+
|
|
23
|
+
it('demonstrates the bug: a prerelease sorts BELOW its own release under plain semver.lte', () => {
|
|
24
|
+
expect(semver.lte(MIGRATION_VERSION, BETA_CLI_VERSION)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('the fix: comparing against the coerced base version includes it correctly', () => {
|
|
28
|
+
expect(semver.lte(MIGRATION_VERSION, baseVersion(BETA_CLI_VERSION))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('a migration registered at a later version than the beta build is still correctly excluded', () => {
|
|
32
|
+
expect(semver.lte('0.24.0', baseVersion(BETA_CLI_VERSION))).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -22,6 +22,9 @@ export interface GenerateZ2HTokenOptions {
|
|
|
22
22
|
|
|
23
23
|
async function promptForToken(): Promise<string> {
|
|
24
24
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
25
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
26
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
27
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
25
28
|
try {
|
|
26
29
|
const raw = await rl.question('Paste your token here, then press Enter:\n> ');
|
|
27
30
|
return raw.trim();
|
|
@@ -65,11 +68,24 @@ async function runLoopbackFlow(pageUrl: string): Promise<string | null> {
|
|
|
65
68
|
|
|
66
69
|
info('waiting for the page to send the token (Ctrl+C to fall back to manual paste)…');
|
|
67
70
|
|
|
71
|
+
// A SIGINT listener suppresses Node's default termination while it is attached. That is
|
|
72
|
+
// what turns the Ctrl+C offered above into the manual-paste fallback instead of a killed
|
|
73
|
+
// CLI. The finally below detaches it, so it covers only this wait.
|
|
74
|
+
let stopListening = (): void => {};
|
|
75
|
+
const cancelled = new Promise<null>(resolve => {
|
|
76
|
+
const onSigint = (): void => resolve(null);
|
|
77
|
+
process.once('SIGINT', onSigint);
|
|
78
|
+
stopListening = () => process.removeListener('SIGINT', onSigint);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// `close()` below rejects a still-pending wait. Swallow it here so the loser of the race
|
|
82
|
+
// never becomes an unhandled rejection, which Node turns into a crash.
|
|
83
|
+
const arrived = loopback.waitForToken(LOOPBACK_TIMEOUT_MS).catch(() => null);
|
|
84
|
+
|
|
68
85
|
try {
|
|
69
|
-
return await
|
|
70
|
-
} catch {
|
|
71
|
-
return null;
|
|
86
|
+
return await Promise.race([arrived, cancelled]);
|
|
72
87
|
} finally {
|
|
88
|
+
stopListening();
|
|
73
89
|
loopback.close();
|
|
74
90
|
}
|
|
75
91
|
}
|
package/src/commands/migrate.ts
CHANGED
|
@@ -24,6 +24,16 @@ interface MigrationsJson {
|
|
|
24
24
|
|
|
25
25
|
// ─── Helpers ───
|
|
26
26
|
|
|
27
|
+
// A prerelease sorts BELOW its own release under plain semver ordering, so
|
|
28
|
+
// comparing raw beta versions here would skip migrations the beta introduced.
|
|
29
|
+
export function baseVersion(version: string): string {
|
|
30
|
+
const parsed = semver.parse(version);
|
|
31
|
+
if (!parsed) {
|
|
32
|
+
throw new Error(`invalid semver version: ${version}`);
|
|
33
|
+
}
|
|
34
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
async function readMigrationState(workspaceHome: string): Promise<MigrationState | null> {
|
|
28
38
|
const statePath = path.join(workspaceHome, STATE_FILE);
|
|
29
39
|
if (!(await pathExists(statePath))) {
|
|
@@ -56,13 +66,14 @@ export async function migrateCommand(cliVersion: string, opts: MigrateOptions =
|
|
|
56
66
|
|
|
57
67
|
const state = await readMigrationState(workspaceHome);
|
|
58
68
|
const lastVersion = state?.lastMigratedVersion ?? '0.0.0';
|
|
69
|
+
const targetVersion = baseVersion(cliVersion);
|
|
59
70
|
|
|
60
|
-
if (!semver.gt(
|
|
71
|
+
if (!semver.gt(targetVersion, lastVersion)) {
|
|
61
72
|
info('workspace is up to date — no migrations pending');
|
|
62
73
|
return;
|
|
63
74
|
}
|
|
64
75
|
|
|
65
|
-
info(`migrating workspace from v${lastVersion} → v${
|
|
76
|
+
info(`migrating workspace from v${lastVersion} → v${targetVersion}...`);
|
|
66
77
|
|
|
67
78
|
// migrations.json is at the package root — two dirs up from dist/migrations/
|
|
68
79
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -75,12 +86,12 @@ export async function migrateCommand(cliVersion: string, opts: MigrateOptions =
|
|
|
75
86
|
|
|
76
87
|
const migrationsJson = (await readJson(migrationsJsonPath)) as MigrationsJson;
|
|
77
88
|
|
|
78
|
-
// Filter to pending migrations (version > lastVersion AND <=
|
|
89
|
+
// Filter to pending migrations (version > lastVersion AND <= targetVersion).
|
|
79
90
|
// CONVENTION: a migration's `version` field must equal the CLI release it
|
|
80
91
|
// ships with (not the feature branch version). This ensures the `gt` check
|
|
81
92
|
// always catches a migration on the exact upgrade that introduces it.
|
|
82
93
|
const pending = Object.entries(migrationsJson.migrations)
|
|
83
|
-
.filter(([, entry]) => semver.gt(entry.version, lastVersion) && semver.lte(entry.version,
|
|
94
|
+
.filter(([, entry]) => semver.gt(entry.version, lastVersion) && semver.lte(entry.version, targetVersion))
|
|
84
95
|
.sort(([, a], [, b]) => semver.compare(a.version, b.version));
|
|
85
96
|
|
|
86
97
|
if (opts.dryRun) {
|
|
@@ -92,7 +103,7 @@ export async function migrateCommand(cliVersion: string, opts: MigrateOptions =
|
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
if (pending.length === 0) {
|
|
95
|
-
await writeMigrationState(workspaceHome, { lastMigratedVersion:
|
|
106
|
+
await writeMigrationState(workspaceHome, { lastMigratedVersion: targetVersion });
|
|
96
107
|
return;
|
|
97
108
|
}
|
|
98
109
|
|
|
@@ -143,6 +154,6 @@ export async function migrateCommand(cliVersion: string, opts: MigrateOptions =
|
|
|
143
154
|
await runYarnInstall(workspaceHome, 'package.json changed');
|
|
144
155
|
}
|
|
145
156
|
|
|
146
|
-
await writeMigrationState(workspaceHome, { lastMigratedVersion:
|
|
157
|
+
await writeMigrationState(workspaceHome, { lastMigratedVersion: targetVersion });
|
|
147
158
|
info('workspace migrations complete.');
|
|
148
159
|
}
|
package/src/commands/tag.ts
CHANGED
|
@@ -56,6 +56,9 @@ async function resolveAppSlug(appArg?: string): Promise<string> {
|
|
|
56
56
|
*/
|
|
57
57
|
export async function promptForTags(appSlug: string, catalog: CatalogEntry[], current: AppTag[]): Promise<string[]> {
|
|
58
58
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
59
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
60
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
61
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
59
62
|
const selected: string[] = [];
|
|
60
63
|
try {
|
|
61
64
|
info(`Tagging "${appSlug}".`);
|