@mondaydotcomorg/z2h-cli 0.31.0 → 0.32.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=llm-migration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-migration.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/llm-migration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ export interface LlmMigrationOptions {
2
+ /** Omit to list every LLM migration instead of printing one guide. */
3
+ migration?: string;
4
+ }
5
+ export declare function llmMigrationCommand(opts: LlmMigrationOptions): Promise<void>;
6
+ //# sourceMappingURL=llm-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-migration.d.ts","sourceRoot":"","sources":["../../src/commands/llm-migration.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlF"}
@@ -0,0 +1,33 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const path = require('node:path');
4
+ const fs = require('fs-extra');
5
+ const util_logger = require('../util/logger.js');
6
+ const util_migrationsJson = require('../util/migrations-json.js');
7
+
8
+ const _interopDefault = e => e && e.__esModule ? e : { default: e };
9
+
10
+ const path__default = /*#__PURE__*/_interopDefault(path);
11
+
12
+ async function llmMigrationCommand(opts) {
13
+ const resolved = await util_migrationsJson.resolveMigrationsJson();
14
+ if (!resolved) {
15
+ throw new Error('migrations.json not found');
16
+ }
17
+ const { migrationsJson, migrationsDir } = resolved;
18
+ if (!opts.migration) {
19
+ const migrations = Object.entries(migrationsJson.migrations)
20
+ .filter(([, entry]) => entry.type === util_migrationsJson.LLM_MIGRATION_TYPE)
21
+ .map(([name, entry]) => ({ name, description: entry.description }));
22
+ util_logger.output(migrations.map(m => `${m.name}\n ${m.description}`).join('\n'), { ok: true, migrations });
23
+ return;
24
+ }
25
+ const entry = migrationsJson.migrations[opts.migration];
26
+ if (entry?.type !== util_migrationsJson.LLM_MIGRATION_TYPE) {
27
+ throw new Error(`no LLM migration named "${opts.migration}"`);
28
+ }
29
+ const guide = await fs.readFile(path__default.default.resolve(migrationsDir, entry.guide), 'utf8');
30
+ util_logger.output(guide, { ok: true, description: entry.description, guide });
31
+ }
32
+
33
+ exports.llmMigrationCommand = llmMigrationCommand;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAiBA,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,CA4FjG"}
@@ -2,20 +2,18 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const node_module = require('node:module');
4
4
  const path = require('node:path');
5
- const node_url = require('node:url');
6
5
  const fs = require('fs-extra');
7
6
  const semver = require('semver');
8
7
  const util_logger = require('../util/logger.js');
9
8
  const util_workspace = require('../util/workspace.js');
9
+ const util_migrationsJson = require('../util/migrations-json.js');
10
10
 
11
- var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
12
11
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
13
12
 
14
13
  const path__default = /*#__PURE__*/_interopDefault(path);
15
14
  const semver__default = /*#__PURE__*/_interopDefault(semver);
16
15
 
17
16
  const STATE_FILE = '.z2h-migrations-state.json';
18
- // ─── Helpers ───
19
17
  // A prerelease sorts BELOW its own release under plain semver ordering, so
20
18
  // comparing raw beta versions here would skip migrations the beta introduced.
21
19
  function baseVersion(version) {
@@ -55,19 +53,14 @@ async function migrateCommand(cliVersion, opts = {}) {
55
53
  return;
56
54
  }
57
55
  util_logger.info(`migrating workspace from v${lastVersion} → v${targetVersion}...`);
58
- // migrations.json is at the package root — two dirs up from dist/migrations/
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))));
60
- const migrationsJsonPath = path__default.default.resolve(thisDir, '..', '..', 'migrations.json');
61
- if (!(await fs.pathExists(migrationsJsonPath))) {
56
+ const resolved = await util_migrationsJson.resolveMigrationsJson();
57
+ if (!resolved) {
62
58
  util_logger.warn('migrations.json not found — skipping migrations');
63
59
  return;
64
60
  }
65
- const migrationsJson = (await fs.readJson(migrationsJsonPath));
66
- // Filter to pending migrations (version > lastVersion AND <= targetVersion).
67
- // CONVENTION: a migration's `version` field must equal the CLI release it
68
- // ships with (not the feature branch version). This ensures the `gt` check
69
- // always catches a migration on the exact upgrade that introduces it.
61
+ const { migrationsJson, migrationsDir } = resolved;
70
62
  const pending = Object.entries(migrationsJson.migrations)
63
+ .filter((pair) => pair[1].type !== util_migrationsJson.LLM_MIGRATION_TYPE)
71
64
  .filter(([, entry]) => semver__default.default.gt(entry.version, lastVersion) && semver__default.default.lte(entry.version, targetVersion))
72
65
  .sort(([, a], [, b]) => semver__default.default.compare(a.version, b.version));
73
66
  if (opts.dryRun) {
@@ -93,7 +86,7 @@ async function migrateCommand(cliVersion, opts = {}) {
93
86
  for (const [name, entry] of pending) {
94
87
  const tree = new FsTree(workspaceHome, false);
95
88
  try {
96
- const factoryPath = path__default.default.resolve(path__default.default.dirname(migrationsJsonPath), entry.migrate);
89
+ const factoryPath = path__default.default.resolve(migrationsDir, entry.migrate);
97
90
  const mod = require(factoryPath);
98
91
  const fn = mod.default || mod;
99
92
  fn(tree);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=llm-migration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-migration.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/__tests__/llm-migration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ export interface LlmMigrationOptions {
2
+ /** Omit to list every LLM migration instead of printing one guide. */
3
+ migration?: string;
4
+ }
5
+ export declare function llmMigrationCommand(opts: LlmMigrationOptions): Promise<void>;
6
+ //# sourceMappingURL=llm-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-migration.d.ts","sourceRoot":"","sources":["../../../src/commands/llm-migration.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlF"}
@@ -0,0 +1,27 @@
1
+ import path from 'node:path';
2
+ import { readFile } from 'fs-extra';
3
+ import { output } from '../util/logger.mjs';
4
+ import { resolveMigrationsJson, LLM_MIGRATION_TYPE } from '../util/migrations-json.mjs';
5
+
6
+ async function llmMigrationCommand(opts) {
7
+ const resolved = await resolveMigrationsJson();
8
+ if (!resolved) {
9
+ throw new Error('migrations.json not found');
10
+ }
11
+ const { migrationsJson, migrationsDir } = resolved;
12
+ if (!opts.migration) {
13
+ const migrations = Object.entries(migrationsJson.migrations)
14
+ .filter(([, entry]) => entry.type === LLM_MIGRATION_TYPE)
15
+ .map(([name, entry]) => ({ name, description: entry.description }));
16
+ output(migrations.map(m => `${m.name}\n ${m.description}`).join('\n'), { ok: true, migrations });
17
+ return;
18
+ }
19
+ const entry = migrationsJson.migrations[opts.migration];
20
+ if (entry?.type !== LLM_MIGRATION_TYPE) {
21
+ throw new Error(`no LLM migration named "${opts.migration}"`);
22
+ }
23
+ const guide = await readFile(path.resolve(migrationsDir, entry.guide), 'utf8');
24
+ output(guide, { ok: true, description: entry.description, guide });
25
+ }
26
+
27
+ export { llmMigrationCommand };
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/commands/migrate.ts"],"names":[],"mappings":"AAiBA,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,CA4FjG"}
@@ -1,13 +1,12 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
3
  import { pathExists, readJson, writeJson } from 'fs-extra';
5
4
  import semver from 'semver';
6
5
  import { warn, info, errorLog } from '../util/logger.mjs';
7
6
  import { getWorkspaceHome, isZ2hWorkspace, ensureWorkspaceNx, runYarnInstall } from '../util/workspace.mjs';
7
+ import { resolveMigrationsJson, LLM_MIGRATION_TYPE } from '../util/migrations-json.mjs';
8
8
 
9
9
  const STATE_FILE = '.z2h-migrations-state.json';
10
- // ─── Helpers ───
11
10
  // A prerelease sorts BELOW its own release under plain semver ordering, so
12
11
  // comparing raw beta versions here would skip migrations the beta introduced.
13
12
  function baseVersion(version) {
@@ -47,19 +46,14 @@ async function migrateCommand(cliVersion, opts = {}) {
47
46
  return;
48
47
  }
49
48
  info(`migrating workspace from v${lastVersion} → v${targetVersion}...`);
50
- // migrations.json is at the package root — two dirs up from dist/migrations/
51
- const thisDir = path.dirname(fileURLToPath(import.meta.url));
52
- const migrationsJsonPath = path.resolve(thisDir, '..', '..', 'migrations.json');
53
- if (!(await pathExists(migrationsJsonPath))) {
49
+ const resolved = await resolveMigrationsJson();
50
+ if (!resolved) {
54
51
  warn('migrations.json not found — skipping migrations');
55
52
  return;
56
53
  }
57
- const migrationsJson = (await readJson(migrationsJsonPath));
58
- // Filter to pending migrations (version > lastVersion AND <= targetVersion).
59
- // CONVENTION: a migration's `version` field must equal the CLI release it
60
- // ships with (not the feature branch version). This ensures the `gt` check
61
- // always catches a migration on the exact upgrade that introduces it.
54
+ const { migrationsJson, migrationsDir } = resolved;
62
55
  const pending = Object.entries(migrationsJson.migrations)
56
+ .filter((pair) => pair[1].type !== LLM_MIGRATION_TYPE)
63
57
  .filter(([, entry]) => semver.gt(entry.version, lastVersion) && semver.lte(entry.version, targetVersion))
64
58
  .sort(([, a], [, b]) => semver.compare(a.version, b.version));
65
59
  if (opts.dryRun) {
@@ -85,7 +79,7 @@ async function migrateCommand(cliVersion, opts = {}) {
85
79
  for (const [name, entry] of pending) {
86
80
  const tree = new FsTree(workspaceHome, false);
87
81
  try {
88
- const factoryPath = path.resolve(path.dirname(migrationsJsonPath), entry.migrate);
82
+ const factoryPath = path.resolve(migrationsDir, entry.migrate);
89
83
  const mod = require(factoryPath);
90
84
  const fn = mod.default || mod;
91
85
  fn(tree);
@@ -15,6 +15,7 @@ import { generateCommand } from './commands/generate.mjs';
15
15
  import { templatesCommand } from './commands/templates.mjs';
16
16
  import { generateZ2HTokenCommand } from './commands/generate-z2h-token.mjs';
17
17
  import { migrateCommand } from './commands/migrate.mjs';
18
+ import { llmMigrationCommand } from './commands/llm-migration.mjs';
18
19
  import { tagCommand } from './commands/tag.mjs';
19
20
  import { editCommand } from './commands/edit.mjs';
20
21
  import { backendValidateCommand, backendInvokeCommand } from './commands/backend.mjs';
@@ -187,6 +188,11 @@ async function main() {
187
188
  .description('Run pending workspace migrations')
188
189
  .option('--dry-run', 'show pending migrations without applying any changes')
189
190
  .action(runCommand({}, (opts) => migrateCommand(version, opts)));
191
+ program
192
+ .command('llm-migration')
193
+ .description('List agent-guided (LLM) migrations, or print one migration guide with --migration')
194
+ .option('--migration <name>', 'name of the LLM migration to print; omit to list all')
195
+ .action(runCommand({}, (opts) => llmMigrationCommand(opts)));
190
196
  program
191
197
  .command('doctor')
192
198
  .description('Check that all Z2H tools, auth, workspace, and plugins are correctly set up')
@@ -0,0 +1,28 @@
1
+ export interface DeterministicMigrationEntry {
2
+ type?: never;
3
+ /** Must equal the CLI release it ships with (not the feature branch version),
4
+ * so the `gt` check in migrate.ts always catches it on the exact upgrade that introduces it. */
5
+ version: string;
6
+ description: string;
7
+ migrate: string;
8
+ }
9
+ export declare const LLM_MIGRATION_TYPE: "llm";
10
+ /** Applied by handing `guide`'s contents to the calling agent. The guide owns detection too:
11
+ * it tells the agent how to find the old pattern and to report "not relevant" if absent. */
12
+ export interface LlmMigrationEntry {
13
+ type: typeof LLM_MIGRATION_TYPE;
14
+ /** Version of the CLI this migration was added at. */
15
+ version: string;
16
+ description: string;
17
+ /** Path (relative to migrations.json) to the markdown guideline for the agent applying this migration. */
18
+ guide: string;
19
+ }
20
+ export type MigrationEntry = DeterministicMigrationEntry | LlmMigrationEntry;
21
+ export interface MigrationsJson {
22
+ migrations: Record<string, MigrationEntry>;
23
+ }
24
+ export declare function resolveMigrationsJson(): Promise<{
25
+ migrationsJson: MigrationsJson;
26
+ migrationsDir: string;
27
+ } | null>;
28
+ //# sourceMappingURL=migrations-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations-json.d.ts","sourceRoot":"","sources":["../../../src/util/migrations-json.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,2BAA2B;IAC1C,IAAI,CAAC,EAAE,KAAK,CAAC;IACb;qGACiG;IACjG,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,kBAAkB,EAAG,KAAc,CAAC;AAEjD;6FAC6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,kBAAkB,CAAC;IAChC,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,0GAA0G;IAC1G,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,cAAc,GAAG,2BAA2B,GAAG,iBAAiB,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;CAC5C;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CAAC;IACrD,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,IAAI,CAAC,CASR"}
@@ -0,0 +1,17 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { pathExists, readJson } from 'fs-extra';
4
+
5
+ const LLM_MIGRATION_TYPE = 'llm';
6
+ async function resolveMigrationsJson() {
7
+ const pkgJsonPath = createRequire(import.meta.url).resolve('@mondaydotcomorg/z2h-cli/package.json');
8
+ const migrationsDir = path.dirname(pkgJsonPath);
9
+ const migrationsJsonPath = path.join(migrationsDir, 'migrations.json');
10
+ if (!(await pathExists(migrationsJsonPath))) {
11
+ return null;
12
+ }
13
+ const migrationsJson = (await readJson(migrationsJsonPath));
14
+ return { migrationsJson, migrationsDir };
15
+ }
16
+
17
+ export { LLM_MIGRATION_TYPE, resolveMigrationsJson };
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ const commands_generate = require('./commands/generate.js');
15
15
  const commands_templates = require('./commands/templates.js');
16
16
  const commands_generateZ2hToken = require('./commands/generate-z2h-token.js');
17
17
  const commands_migrate = require('./commands/migrate.js');
18
+ const commands_llmMigration = require('./commands/llm-migration.js');
18
19
  const commands_tag = require('./commands/tag.js');
19
20
  const commands_edit = require('./commands/edit.js');
20
21
  const commands_backend = require('./commands/backend.js');
@@ -193,6 +194,11 @@ async function main() {
193
194
  .description('Run pending workspace migrations')
194
195
  .option('--dry-run', 'show pending migrations without applying any changes')
195
196
  .action(util_runCommand.runCommand({}, (opts) => commands_migrate.migrateCommand(version, opts)));
197
+ program
198
+ .command('llm-migration')
199
+ .description('List agent-guided (LLM) migrations, or print one migration guide with --migration')
200
+ .option('--migration <name>', 'name of the LLM migration to print; omit to list all')
201
+ .action(util_runCommand.runCommand({}, (opts) => commands_llmMigration.llmMigrationCommand(opts)));
196
202
  program
197
203
  .command('doctor')
198
204
  .description('Check that all Z2H tools, auth, workspace, and plugins are correctly set up')
@@ -0,0 +1,28 @@
1
+ export interface DeterministicMigrationEntry {
2
+ type?: never;
3
+ /** Must equal the CLI release it ships with (not the feature branch version),
4
+ * so the `gt` check in migrate.ts always catches it on the exact upgrade that introduces it. */
5
+ version: string;
6
+ description: string;
7
+ migrate: string;
8
+ }
9
+ export declare const LLM_MIGRATION_TYPE: "llm";
10
+ /** Applied by handing `guide`'s contents to the calling agent. The guide owns detection too:
11
+ * it tells the agent how to find the old pattern and to report "not relevant" if absent. */
12
+ export interface LlmMigrationEntry {
13
+ type: typeof LLM_MIGRATION_TYPE;
14
+ /** Version of the CLI this migration was added at. */
15
+ version: string;
16
+ description: string;
17
+ /** Path (relative to migrations.json) to the markdown guideline for the agent applying this migration. */
18
+ guide: string;
19
+ }
20
+ export type MigrationEntry = DeterministicMigrationEntry | LlmMigrationEntry;
21
+ export interface MigrationsJson {
22
+ migrations: Record<string, MigrationEntry>;
23
+ }
24
+ export declare function resolveMigrationsJson(): Promise<{
25
+ migrationsJson: MigrationsJson;
26
+ migrationsDir: string;
27
+ } | null>;
28
+ //# sourceMappingURL=migrations-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations-json.d.ts","sourceRoot":"","sources":["../../src/util/migrations-json.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,2BAA2B;IAC1C,IAAI,CAAC,EAAE,KAAK,CAAC;IACb;qGACiG;IACjG,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,kBAAkB,EAAG,KAAc,CAAC;AAEjD;6FAC6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,kBAAkB,CAAC;IAChC,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,0GAA0G;IAC1G,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,cAAc,GAAG,2BAA2B,GAAG,iBAAiB,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;CAC5C;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CAAC;IACrD,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,IAAI,CAAC,CASR"}
@@ -0,0 +1,25 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const node_module = require('node:module');
4
+ const path = require('node:path');
5
+ const fs = require('fs-extra');
6
+
7
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
+ const _interopDefault = e => e && e.__esModule ? e : { default: e };
9
+
10
+ const path__default = /*#__PURE__*/_interopDefault(path);
11
+
12
+ const LLM_MIGRATION_TYPE = 'llm';
13
+ async function resolveMigrationsJson() {
14
+ const pkgJsonPath = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('util/migrations-json.js', document.baseURI).href))).resolve('@mondaydotcomorg/z2h-cli/package.json');
15
+ const migrationsDir = path__default.default.dirname(pkgJsonPath);
16
+ const migrationsJsonPath = path__default.default.join(migrationsDir, 'migrations.json');
17
+ if (!(await fs.pathExists(migrationsJsonPath))) {
18
+ return null;
19
+ }
20
+ const migrationsJson = (await fs.readJson(migrationsJsonPath));
21
+ return { migrationsJson, migrationsDir };
22
+ }
23
+
24
+ exports.LLM_MIGRATION_TYPE = LLM_MIGRATION_TYPE;
25
+ exports.resolveMigrationsJson = resolveMigrationsJson;
package/migrations.json CHANGED
@@ -34,6 +34,12 @@
34
34
  "version": "0.19.0",
35
35
  "description": "Rewrite html-embed apps' iframe src to go through the host's same-origin /z2h-assets proxy instead of the raw CDN bucket URL",
36
36
  "migrate": "./dist/migrations/007-proxy-iframe-src"
37
+ },
38
+ "snowflake-service-to-backend-runner": {
39
+ "type": "llm",
40
+ "version": "0.29.3",
41
+ "description": "Migrate off the deprecated direct bigbrain-zth /snowflake/query pattern onto a zth-backend-runner handler (ctx.api.v1.snowflake)",
42
+ "guide": "./src/migrations-llm/snowflake-service-to-backend-runner.md"
37
43
  }
38
44
  }
39
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -0,0 +1,98 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { mkdtemp, rm, writeFile } from 'fs-extra';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ import { llmMigrationCommand } from '../llm-migration';
7
+ import { output } from '../../util/logger';
8
+ import { resolveMigrationsJson } from '../../util/migrations-json';
9
+
10
+ vi.mock('../../util/logger', () => ({
11
+ output: vi.fn(),
12
+ }));
13
+ vi.mock('../../util/migrations-json', () => ({
14
+ resolveMigrationsJson: vi.fn(),
15
+ LLM_MIGRATION_TYPE: 'llm',
16
+ }));
17
+
18
+ const outputMock = vi.mocked(output);
19
+ const resolveMock = vi.mocked(resolveMigrationsJson);
20
+
21
+ describe('llmMigrationCommand', () => {
22
+ let tmp: string;
23
+
24
+ beforeEach(async () => {
25
+ tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-llm-migration-'));
26
+ outputMock.mockClear();
27
+ resolveMock.mockReset();
28
+ });
29
+
30
+ afterEach(async () => {
31
+ await rm(tmp, { recursive: true, force: true });
32
+ });
33
+
34
+ it('throws when migrations.json is missing', async () => {
35
+ resolveMock.mockResolvedValue(null);
36
+
37
+ await expect(llmMigrationCommand({ migration: 'any' })).rejects.toThrow(/migrations.json not found/);
38
+ });
39
+
40
+ it('throws when there is no entry, or the entry is not an llm migration', async () => {
41
+ resolveMock.mockResolvedValue({
42
+ migrationsJson: { migrations: { 'deterministic-one': { version: '0.1.0', description: 'd', migrate: 'x.js' } } },
43
+ migrationsDir: tmp,
44
+ });
45
+
46
+ await expect(llmMigrationCommand({ migration: 'missing' })).rejects.toThrow(/no LLM migration named/);
47
+ await expect(llmMigrationCommand({ migration: 'deterministic-one' })).rejects.toThrow(/no LLM migration named/);
48
+ });
49
+
50
+ it('lists only llm migrations when --migration is omitted', async () => {
51
+ resolveMock.mockResolvedValue({
52
+ migrationsJson: {
53
+ migrations: {
54
+ 'deterministic-one': { version: '0.1.0', description: 'd', migrate: 'x.js' },
55
+ alpha: { type: 'llm', version: '0.30.0', description: 'first', guide: 'a.md' },
56
+ beta: { type: 'llm', version: '0.30.0', description: 'second', guide: 'b.md' },
57
+ },
58
+ },
59
+ migrationsDir: tmp,
60
+ });
61
+
62
+ await llmMigrationCommand({});
63
+
64
+ expect(outputMock).toHaveBeenCalledWith(expect.stringContaining('alpha\n first'), {
65
+ ok: true,
66
+ migrations: [
67
+ { name: 'alpha', description: 'first' },
68
+ { name: 'beta', description: 'second' },
69
+ ],
70
+ });
71
+ });
72
+
73
+ it('outputs the guide resolved relative to migrations.json', async () => {
74
+ await writeFile(path.join(tmp, 'guide.md'), 'guide contents');
75
+ resolveMock.mockResolvedValue({
76
+ migrationsJson: {
77
+ migrations: { on: { type: 'llm', version: '0.30.0', description: 'on', guide: 'guide.md' } },
78
+ },
79
+ migrationsDir: tmp,
80
+ });
81
+
82
+ await llmMigrationCommand({ migration: 'on' });
83
+
84
+ expect(outputMock).toHaveBeenCalledWith('guide contents', { ok: true, description: 'on', guide: 'guide contents' });
85
+ });
86
+
87
+ it('propagates a missing guide file', async () => {
88
+ resolveMock.mockResolvedValue({
89
+ migrationsJson: {
90
+ migrations: { broken: { type: 'llm', version: '0.30.0', description: 'broken', guide: 'nope.md' } },
91
+ },
92
+ migrationsDir: tmp,
93
+ });
94
+
95
+ await expect(llmMigrationCommand({ migration: 'broken' })).rejects.toThrow(/ENOENT/);
96
+ expect(outputMock).not.toHaveBeenCalled();
97
+ });
98
+ });
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import { readFile } from 'fs-extra';
3
+ import { output } from '../util/logger';
4
+ import { resolveMigrationsJson, LLM_MIGRATION_TYPE } from '../util/migrations-json';
5
+
6
+ export interface LlmMigrationOptions {
7
+ /** Omit to list every LLM migration instead of printing one guide. */
8
+ migration?: string;
9
+ }
10
+
11
+ export async function llmMigrationCommand(opts: LlmMigrationOptions): Promise<void> {
12
+ const resolved = await resolveMigrationsJson();
13
+ if (!resolved) {
14
+ throw new Error('migrations.json not found');
15
+ }
16
+ const { migrationsJson, migrationsDir } = resolved;
17
+
18
+ if (!opts.migration) {
19
+ const migrations = Object.entries(migrationsJson.migrations)
20
+ .filter(([, entry]) => entry.type === LLM_MIGRATION_TYPE)
21
+ .map(([name, entry]) => ({ name, description: entry.description }));
22
+ output(migrations.map(m => `${m.name}\n ${m.description}`).join('\n'), { ok: true, migrations });
23
+ return;
24
+ }
25
+
26
+ const entry = migrationsJson.migrations[opts.migration];
27
+ if (entry?.type !== LLM_MIGRATION_TYPE) {
28
+ throw new Error(`no LLM migration named "${opts.migration}"`);
29
+ }
30
+
31
+ const guide = await readFile(path.resolve(migrationsDir, entry.guide), 'utf8');
32
+ output(guide, { ok: true, description: entry.description, guide });
33
+ }
@@ -1,10 +1,11 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
3
  import { pathExists, readJson, writeJson } from 'fs-extra';
5
4
  import semver from 'semver';
6
5
  import { errorLog, info, warn } from '../util/logger';
7
6
  import { getWorkspaceHome, isZ2hWorkspace, ensureWorkspaceNx, runYarnInstall } from '../util/workspace';
7
+ import { resolveMigrationsJson, LLM_MIGRATION_TYPE } from '../util/migrations-json';
8
+ import type { DeterministicMigrationEntry } from '../util/migrations-json';
8
9
 
9
10
  const STATE_FILE = '.z2h-migrations-state.json';
10
11
 
@@ -12,18 +13,6 @@ interface MigrationState {
12
13
  lastMigratedVersion: string;
13
14
  }
14
15
 
15
- interface MigrationEntry {
16
- version: string;
17
- description: string;
18
- migrate: string;
19
- }
20
-
21
- interface MigrationsJson {
22
- migrations: Record<string, MigrationEntry>;
23
- }
24
-
25
- // ─── Helpers ───
26
-
27
16
  // A prerelease sorts BELOW its own release under plain semver ordering, so
28
17
  // comparing raw beta versions here would skip migrations the beta introduced.
29
18
  export function baseVersion(version: string): string {
@@ -75,22 +64,15 @@ export async function migrateCommand(cliVersion: string, opts: MigrateOptions =
75
64
 
76
65
  info(`migrating workspace from v${lastVersion} → v${targetVersion}...`);
77
66
 
78
- // migrations.json is at the package root — two dirs up from dist/migrations/
79
- const thisDir = path.dirname(fileURLToPath(import.meta.url));
80
- const migrationsJsonPath = path.resolve(thisDir, '..', '..', 'migrations.json');
81
-
82
- if (!(await pathExists(migrationsJsonPath))) {
67
+ const resolved = await resolveMigrationsJson();
68
+ if (!resolved) {
83
69
  warn('migrations.json not found — skipping migrations');
84
70
  return;
85
71
  }
72
+ const { migrationsJson, migrationsDir } = resolved;
86
73
 
87
- const migrationsJson = (await readJson(migrationsJsonPath)) as MigrationsJson;
88
-
89
- // Filter to pending migrations (version > lastVersion AND <= targetVersion).
90
- // CONVENTION: a migration's `version` field must equal the CLI release it
91
- // ships with (not the feature branch version). This ensures the `gt` check
92
- // always catches a migration on the exact upgrade that introduces it.
93
74
  const pending = Object.entries(migrationsJson.migrations)
75
+ .filter((pair): pair is [string, DeterministicMigrationEntry] => pair[1].type !== LLM_MIGRATION_TYPE)
94
76
  .filter(([, entry]) => semver.gt(entry.version, lastVersion) && semver.lte(entry.version, targetVersion))
95
77
  .sort(([, a], [, b]) => semver.compare(a.version, b.version));
96
78
 
@@ -128,7 +110,7 @@ export async function migrateCommand(cliVersion: string, opts: MigrateOptions =
128
110
  for (const [name, entry] of pending) {
129
111
  const tree = new FsTree(workspaceHome, false);
130
112
  try {
131
- const factoryPath = path.resolve(path.dirname(migrationsJsonPath), entry.migrate);
113
+ const factoryPath = path.resolve(migrationsDir, entry.migrate);
132
114
  const mod = require(factoryPath);
133
115
  const fn = mod.default || mod;
134
116
  fn(tree);
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import { generateCommand, GenerateOptions } from './commands/generate';
15
15
  import { templatesCommand } from './commands/templates';
16
16
  import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
17
17
  import { migrateCommand } from './commands/migrate';
18
+ import { llmMigrationCommand } from './commands/llm-migration';
18
19
  import { tagCommand } from './commands/tag';
19
20
  import { editCommand } from './commands/edit';
20
21
  import { backendInvokeCommand, backendValidateCommand } from './commands/backend';
@@ -297,6 +298,12 @@ async function main(): Promise<void> {
297
298
  .option('--dry-run', 'show pending migrations without applying any changes')
298
299
  .action(runCommand({}, (opts: { dryRun?: boolean }) => migrateCommand(version, opts)));
299
300
 
301
+ program
302
+ .command('llm-migration')
303
+ .description('List agent-guided (LLM) migrations, or print one migration guide with --migration')
304
+ .option('--migration <name>', 'name of the LLM migration to print; omit to list all')
305
+ .action(runCommand({}, (opts: { migration?: string }) => llmMigrationCommand(opts)));
306
+
300
307
  program
301
308
  .command('doctor')
302
309
  .description('Check that all Z2H tools, auth, workspace, and plugins are correctly set up')
@@ -0,0 +1,168 @@
1
+ # Migration: snowflake-service-to-backend-runner
2
+
3
+ You are migrating one Z2H consumer app off a deprecated data-fetching pattern. Detection is
4
+ your job too: find the old pattern first (see Detect), then rewrite, verify, and check for edit
5
+ collisions before reporting done (see Verify).
6
+
7
+ ## Detect (do this first)
8
+
9
+ Grep the app's `src/` for the endpoint path:
10
+
11
+ ```bash
12
+ cd "$APP_DIR"
13
+ grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.html' \
14
+ '/snowflake/query' src/
15
+ ```
16
+
17
+ - **No matches** → the app does not use the deprecated pattern (or is already migrated). Do
18
+ nothing and report **not applicable**.
19
+ - **Matches** → the app is in scope. The base URL is built one of three ways; grep for whichever
20
+ the matched files use so you find every file involved (helper and fetch often live apart):
21
+ - `getBigBrainAPI(` — React apps, via `bigbrainBaseUrl` (often wrapped in a `getBaseUrl()`)
22
+ - `window.location.origin` — html-embed apps (`getBigBrainAPI` is unavailable in iframes)
23
+ - a hardcoded `https://…/snowflake/query` URL
24
+
25
+ ## Read first: `z2h:backend-development`
26
+
27
+ The target pattern is an app backend handler, and the authoritative guide for writing one is the
28
+ `z2h:backend-development` skill. Read it before touching any file — invoke the skill if you have
29
+ the Skill tool, otherwise read `z2h/skills/backend-development/SKILL.md` from the Z2H plugin
30
+ checkout. This document only tells you *what* to move; that skill tells you *how* handlers must
31
+ be written (file layout, the `ctx` surface, hard rules, how the frontend calls them, error
32
+ handling). Where the two disagree, the skill wins.
33
+
34
+ Two sections there matter most for this migration:
35
+
36
+ - **Hard rules for handler code** — one file per operation, a plain global `handler(ctx)` with no
37
+ `export`/`module.exports`, no imports of any kind, JSON in / JSON out.
38
+ - **The `ctx` API** — the exact `ctx.api.v1.snowflake.*` signature lives in
39
+ `packages/z2h-cli/handler-api/backend-runner/channels/snowflake-channel/snowflake.channel.ts`.
40
+ Read it rather than guessing the method name or return shape.
41
+
42
+ ## Idempotency (required)
43
+
44
+ This migration must be safe to run on an app in any state — untouched, partially migrated by an
45
+ earlier interrupted run, or already fully migrated. Concretely:
46
+
47
+ - Work from what's in the code now, not from assumptions about the "before" shape. If a handler
48
+ for a query already exists under `backend/handlers/`, reuse it instead of creating a duplicate.
49
+ If some call sites already go through `callBackend`, leave them alone.
50
+ - Never leave a half-state behind. Either every step below completes or you report failed — don't
51
+ report done with the old `runQuery` still in place "for later".
52
+ - Rewrite every call site the Detect step found, not just the first.
53
+ - Done means: **the Detect grep returns nothing.** Re-run it before reporting done.
54
+
55
+ ## Old pattern (remove)
56
+
57
+ The app fetches Snowflake data directly from the frontend via the generic `bigbrain-zth` endpoint:
58
+
59
+ ```typescript
60
+ // src/queries.ts (or similar) — DEPRECATED
61
+ import { getBigBrainAPI } from '@mondaydotcomorg/trident-runtime';
62
+
63
+ export async function runQuery(sql: string) {
64
+ const baseUrl = getBigBrainAPI()?.contextService?.bigbrainBaseUrl;
65
+ const res = await fetch(`${baseUrl}/snowflake/query`, {
66
+ method: 'POST',
67
+ body: JSON.stringify({ sql }),
68
+ });
69
+ return res.json();
70
+ }
71
+ ```
72
+
73
+ This pattern is banned for new code (see `z2h:feature-development`'s data-fetching hierarchy) and
74
+ is not available at all inside html-embed iframes.
75
+
76
+ ## New pattern (target)
77
+
78
+ SQL moves server-side into an app backend handler, called from the frontend through the Z2H SDK.
79
+
80
+ 1. **Create a handler** at `backend/handlers/<name>.js`, following the handler rules in
81
+ `z2h:backend-development`. Move the SQL that was in `queries.ts` into it, and query through
82
+ `ctx.api.v1.snowflake`:
83
+
84
+ ```javascript
85
+ // backend/handlers/<name>.js — plain global `handler`, no export, no imports
86
+ async function handler(ctx) {
87
+ const rows = await ctx.api.v1.snowflake.query('SELECT ...', undefined, { identifier: '<name>' });
88
+ return rows;
89
+ }
90
+ ```
91
+
92
+ Pick `<name>` from what the query returns (e.g. `get_encounter`, `list_boards`) — not `runQuery`
93
+ or anything generic; one handler per distinct query/shape, matching existing app conventions.
94
+ The filename (without `.js`) is exactly what the frontend passes to `callBackend`. No scaffold
95
+ command exists — if the app has no `backend/` folder yet, just create `backend/handlers/`.
96
+
97
+ Always pass `identifier` — a short kebab-case string (`[a-z0-9-_]`, max 64 chars), not a
98
+ number; the handler name is a good default. `z2h:backend-development`'s hard rules require it
99
+ on every `ctx.api.v1.snowflake.query` call — it's optional in the API signature, but skipping
100
+ it leaves the query identified by a hash of its SQL, so every later edit to the query makes it
101
+ a new query in logs and metrics. A stable identifier keeps its history across edits.
102
+
103
+ 2. **Replace every call site.** Wherever the frontend called `runQuery(sql)` (or equivalent),
104
+ call the handler through the SDK instead (see *Calling the backend from the frontend* in
105
+ `z2h:backend-development` for error handling with `isBackendError`):
106
+
107
+ **React apps:**
108
+ ```typescript
109
+ import { createZ2HSDK } from '@mondaydotcomorg/z2h-runtime-sdk';
110
+
111
+ const rows = await createZ2HSDK().callBackend('<name>', { /* params, not raw SQL */ });
112
+ ```
113
+
114
+ **html-embed apps (`page.html`):**
115
+ ```javascript
116
+ const rows = await window.__z2hSdk__.callBackend('<name>', { /* params */ });
117
+ ```
118
+
119
+ 3. **Delete the old file** (`src/queries.ts` or equivalent) once nothing imports it, and remove
120
+ the now-unused `getBigBrainAPI` import from anywhere else in `src/`.
121
+
122
+ 4. **Parameterize, don't template.** If the old `runQuery` took a raw SQL string built from
123
+ variables (string interpolation), do NOT ship the handler taking raw SQL from the frontend —
124
+ that's the same trust boundary the old pattern already had, just moved. Have the frontend pass
125
+ structured params (ids, filters) and build/parameterize the SQL inside the handler.
126
+
127
+ ## Verify
128
+
129
+ Two checks, in order. Fix any failure before moving on.
130
+
131
+ 1. **Handler validation** — validates every handler under `backend/handlers/` (syntax, isolate
132
+ rules, known integrations, static-SQL check) without deploying:
133
+
134
+ ```bash
135
+ cd "$APP_DIR"
136
+ z2h-cli backend validate
137
+ ```
138
+
139
+ Fix any error it reports (minimal fix, re-run).
140
+
141
+ Also confirm every `ctx.api.v1.snowflake.query` call you wrote passes `options.identifier` —
142
+ `z2h-cli backend validate` does not check for this, so grep for it yourself:
143
+
144
+ ```bash
145
+ grep -n 'snowflake.query' backend/handlers/*.js
146
+ ```
147
+
148
+ For each match, check the call includes an `identifier` in its `options` argument. Add one if
149
+ missing before moving on.
150
+
151
+ 2. **Build** — confirms the frontend typechecks and bundles after the rewrite:
152
+
153
+ ```bash
154
+ z2h-cli deploy --dry-run
155
+ ```
156
+
157
+ Fix any typecheck/lint failure (minimal fix, re-run).
158
+
159
+ 3. **Collision check (required, run last)** — for every file you edited during this migration,
160
+ re-read its current contents and compare against what you last wrote there. If anything
161
+ differs from what you expect — the app owner edited that file while you were working —
162
+ discard your changes for this run and report "failed" with the reason: "The app was edited
163
+ while the update was in progress, so it wasn't applied — run this update again once things
164
+ have settled." Do not attempt to reconcile or partially apply; the idempotency contract above
165
+ means this migration is always safe to retry cleanly from scratch on the next touch.
166
+
167
+ Do not deploy or commit as part of this migration — that's the normal feature-development flow's
168
+ job, once the person driving this session is ready to ship.
@@ -0,0 +1,45 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { pathExists, readJson } from 'fs-extra';
4
+
5
+ export interface DeterministicMigrationEntry {
6
+ type?: never;
7
+ /** Must equal the CLI release it ships with (not the feature branch version),
8
+ * so the `gt` check in migrate.ts always catches it on the exact upgrade that introduces it. */
9
+ version: string;
10
+ description: string;
11
+ migrate: string;
12
+ }
13
+
14
+ export const LLM_MIGRATION_TYPE = 'llm' as const;
15
+
16
+ /** Applied by handing `guide`'s contents to the calling agent. The guide owns detection too:
17
+ * it tells the agent how to find the old pattern and to report "not relevant" if absent. */
18
+ export interface LlmMigrationEntry {
19
+ type: typeof LLM_MIGRATION_TYPE;
20
+ /** Version of the CLI this migration was added at. */
21
+ version: string;
22
+ description: string;
23
+ /** Path (relative to migrations.json) to the markdown guideline for the agent applying this migration. */
24
+ guide: string;
25
+ }
26
+
27
+ export type MigrationEntry = DeterministicMigrationEntry | LlmMigrationEntry;
28
+
29
+ export interface MigrationsJson {
30
+ migrations: Record<string, MigrationEntry>;
31
+ }
32
+
33
+ export async function resolveMigrationsJson(): Promise<{
34
+ migrationsJson: MigrationsJson;
35
+ migrationsDir: string;
36
+ } | null> {
37
+ const pkgJsonPath = createRequire(import.meta.url).resolve('@mondaydotcomorg/z2h-cli/package.json');
38
+ const migrationsDir = path.dirname(pkgJsonPath);
39
+ const migrationsJsonPath = path.join(migrationsDir, 'migrations.json');
40
+ if (!(await pathExists(migrationsJsonPath))) {
41
+ return null;
42
+ }
43
+ const migrationsJson = (await readJson(migrationsJsonPath)) as MigrationsJson;
44
+ return { migrationsJson, migrationsDir };
45
+ }