Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions .agents/skills/mops-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ args = ["--default-persistent-actors", "-W=M0223,M0236,M0237"]

[canisters.backend]
main = "src/backend/main.mo"
args = ["--enhanced-migration=src/backend/migrations", "-A=M0254"]

[canisters.backend.migrations]
chain = "src/backend/migrations"
next = "src/backend/next-migration" # optional — needed for `mops migrate new/freeze`
check-limit = 1
build-limit = 100

[canisters.backend.check-stable]
path = ".old/src/backend/dist/backend.most"
Expand Down Expand Up @@ -62,8 +67,9 @@ Flags are applied in this order (later overrides earlier):

1. `[moc].args` — global, all commands (check, build, test, etc.)
2. `[build].args` — build only (e.g. `--release`)
3. `[canisters.<name>].args` — per-canister (e.g. `--enhanced-migration=...`)
4. CLI `-- <flags>` — one-off overrides
3. `[canisters.<name>.migrations]` — auto-injected `--enhanced-migration` (managed by mops)
4. `[canisters.<name>].args` — per-canister
5. CLI `-- <flags>` — one-off overrides

## Core Commands

Expand Down Expand Up @@ -125,6 +131,21 @@ mops toolchain bin moc # print path to binary

**Agent note**: `toolchain use <tool>` without a version opens an interactive picker — do not use in scripts or agents. Always pass a version or `latest`. `toolchain update` only works when the tool already has a `[toolchain]` entry.

### `mops migrate`

Manage enhanced migration chains:

```bash
mops migrate new AddEmail # create a new migration file in next-migration/
mops migrate new AddEmail backend # specify canister explicitly
mops migrate freeze # move next-migration to the permanent chain
mops migrate freeze backend # specify canister explicitly
```

When `[canisters.<name>.migrations]` is configured, `mops check`, `mops build`, and `mops check-stable` automatically inject `--enhanced-migration`. Do not add `--enhanced-migration` to `[canisters.<name>].args` when using managed migrations — mops will error.

Typical workflow: make a breaking change → `mops check` fails with a hint → `mops migrate new Name` → edit migration → `mops check` passes → `mops build` → deploy → `mops migrate freeze`.

### `mops remove <package>`

```bash
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This file provides guidance to AI coding agents when working with code in this r
- **Update the changelog.** Add entries under `## Next` in `cli/CHANGELOG.md` for any user-facing CLI changes.
- **Keep skills up to date.** When changing CLI commands or workflows, update `.agents/skills/mops-cli/SKILL.md` to match.
- **Pre-commit hook** runs `lint-staged + npm run check` via husky — fix TypeScript/lint errors before committing.
- **Snapshot testing strategy**: Use Jest snapshots (`cliSnapshot` / `toMatchSnapshot`) for the main use cases so the full CLI output is committed and reviewable. Corner-case and error-path tests should use targeted assertions (`toMatch`, `toBe`) without snapshots to avoid cluttering the snapshot file.

## What this repo is

Expand Down
5 changes: 5 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Mops CLI Changelog

## Next
- Add `mops migrate new <Name>` and `mops migrate freeze` commands for managing enhanced migration chains
- Add `[canisters.<name>.migrations]` config section with `chain`, `next`, `check-limit`, and `build-limit` fields
- `mops check`, `mops build`, and `mops check-stable` now auto-inject `--enhanced-migration` when `[migrations]` is configured
- `mops check` and `mops check-stable` emit a hint to create a migration when a stable compatibility check fails and `[migrations]` is configured
- Migration chain trimming: only the last N migrations are passed to `moc` based on `check-limit`/`build-limit` settings

## 2.10.0
- `mops check` and `mops check-stable` now apply per-canister `[canisters.<name>].args` (previously only `mops build` applied them)
Expand Down
24 changes: 24 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
importPem,
setUserProp,
} from "./commands/user.js";
import { migrateNew, migrateFreeze } from "./commands/migrate.js";
import { watch } from "./commands/watch/watch.js";
import {
apiVersion,
Expand Down Expand Up @@ -707,6 +708,29 @@ toolchainCommand

program.addCommand(toolchainCommand);

// migrate
const migrateCommand = new Command("migrate").description(
"Manage enhanced migration chains",
);

migrateCommand
.command("new <name> [canister]")
.description("Create a new migration file in the next-migration directory")
.action(async (name, canister) => {
checkConfigFile(true);
await migrateNew(name, canister);
});

migrateCommand
.command("freeze [canister]")
.description("Move the next migration into the frozen chain")
.action(async (canister) => {
checkConfigFile(true);
await migrateFreeze(canister);
});

program.addCommand(migrateCommand);

// self
const selfCommand = new Command("self").description("Mops CLI management");

Expand Down
11 changes: 10 additions & 1 deletion cli/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
resolveCanisterConfigs,
validateCanisterArgs,
} from "../helpers/resolve-canisters.js";
import { prepareMigrationArgs } from "../helpers/migrations.js";
import { CanisterConfig, Config } from "../types.js";
import { CustomSection, getWasmBindings } from "../wasm.js";
import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
Expand Down Expand Up @@ -87,6 +88,12 @@ export async function build(
};
process.on("exit", exitCleanup);

const migration = await prepareMigrationArgs(
canister.migrations,
canisterName,
"build",
options.verbose,
);
try {
let args = [
"-c",
Expand All @@ -97,6 +104,7 @@ export async function build(
motokoPath,
...(await sourcesArgs()).flat(),
...getGlobalMocArgs(config),
...migration.migrationArgs,
];
args.push(
...collectExtraArgs(config, canister, canisterName, options.extraArgs),
Expand Down Expand Up @@ -199,6 +207,7 @@ export async function build(
);
}
} finally {
await migration.cleanup();
process.removeListener("exit", exitCleanup);
try {
await release?.();
Expand Down Expand Up @@ -238,7 +247,7 @@ function collectExtraArgs(
args.push(...config.build.args);
}
if (canister.args) {
validateCanisterArgs(canister, canisterName);
validateCanisterArgs(canister, canisterName, config);
args.push(...canister.args);
}
if (extraArgs) {
Expand Down
73 changes: 52 additions & 21 deletions cli/commands/check-stable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { rm } from "node:fs/promises";
import chalk from "chalk";
import { execa } from "execa";
import { cliError } from "../error.js";
import { prepareMigrationArgs } from "../helpers/migrations.js";
import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
import { CanisterConfig } from "../types.js";
import {
Expand Down Expand Up @@ -70,17 +71,28 @@ export async function checkStable(
cliError(`No main file specified for canister '${name}' in mops.toml`);
}

validateCanisterArgs(canister, name);
validateCanisterArgs(canister, name, config);

await runStableCheck({
oldFile,
canisterMain: resolveConfigPath(canister.main),
canisterName: name,
mocPath,
globalMocArgs,
canisterArgs: canister.args ?? [],
options,
});
const migration = await prepareMigrationArgs(
canister.migrations,
name,
"check",
options.verbose,
);
try {
await runStableCheck({
oldFile,
canisterMain: resolveConfigPath(canister.main),
canisterName: name,
mocPath,
globalMocArgs,
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
options,
hasMigrations: !!canister.migrations,
});
} finally {
await migration.cleanup();
}
return;
}

Expand All @@ -95,24 +107,35 @@ export async function checkStable(
cliError(`No main file specified for canister '${name}' in mops.toml`);
}

validateCanisterArgs(canister, name);
validateCanisterArgs(canister, name, config);
const stablePath = resolveStablePath(canister, name, {
required: !!canisterNames,
});
if (!stablePath) {
continue;
}

await runStableCheck({
oldFile: stablePath,
canisterMain: resolveConfigPath(canister.main),
canisterName: name,
mocPath,
globalMocArgs,
canisterArgs: canister.args ?? [],
sources,
options,
});
const migration = await prepareMigrationArgs(
canister.migrations,
name,
"check",
options.verbose,
);
try {
await runStableCheck({
oldFile: stablePath,
canisterMain: resolveConfigPath(canister.main),
canisterName: name,
mocPath,
globalMocArgs,
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
sources,
options,
hasMigrations: !!canister.migrations,
});
} finally {
await migration.cleanup();
}
checked++;
}

Expand All @@ -136,6 +159,7 @@ export interface RunStableCheckParams {
canisterArgs: string[];
sources?: string[];
options?: Partial<CheckStableOptions>;
hasMigrations?: boolean;
}

export async function runStableCheck(
Expand Down Expand Up @@ -204,6 +228,13 @@ export async function runStableCheck(
if (result.stderr) {
console.error(result.stderr);
}
if (params.hasMigrations) {
console.error(
chalk.yellow(
"Hint: You may need a migration. Run `mops migrate new <Name>` to create one.",
),
);
}
cliError(
`✗ Stable compatibility check failed for canister '${canisterName}'`,
);
Expand Down
Loading
Loading