ic-mops 1.1.2 → 1.3.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Mops CLI Changelog
2
2
 
3
+ ## 1.2.0
4
+ - Removed `mops transfer-ownership` command
5
+ - Added `mops owner` command to manage package owners ([docs](https://docs.mops.one/cli/mops-owner))
6
+ - Added `mops maintainers` command to manage package maintainers ([docs](https://docs.mops.one/cli/mops-maintainers))
7
+ - Added experimental support for pocket-ic replica that comes with dfx in `mops test` command ([docs](https://docs.mops.one/cli/mops-test#--replica))
8
+ - Added flag `--verbose` to `mops test` command to show replica logs
9
+ - Fixed bug where `mops watch` would fail if dfx.json did not exist
10
+ - Fixed bug with local dependencies without `mops.toml` file
11
+
3
12
  ## 1.1.2
4
13
  - Fixed `{MOPS_ENV}` substitution in local package path
5
14
 
package/bundle/cli.tgz CHANGED
Binary file
package/cli.ts CHANGED
@@ -21,12 +21,13 @@ import {sync} from './commands/sync.js';
21
21
  import {outdated} from './commands/outdated.js';
22
22
  import {update} from './commands/update.js';
23
23
  import {bench} from './commands/bench.js';
24
- import {transferOwnership} from './commands/transfer-ownership.js';
25
24
  import {toolchain} from './commands/toolchain/index.js';
26
25
  import {Tool} from './types.js';
27
26
  import * as self from './commands/self.js';
28
27
  import {resolvePackages} from './resolve-packages.js';
29
28
  import {watch} from './commands/watch/watch.js';
29
+ import {addOwner, printOwners, removeOwner} from './commands/owner.js';
30
+ import {addMaintainer, printMaintainers, removeMaintainer} from './commands/maintainer.js';
30
31
 
31
32
  declare global {
32
33
  // eslint-disable-next-line no-var
@@ -214,6 +215,7 @@ program
214
215
  .addOption(new Option('--mode <mode>', 'Test mode').choices(['interpreter', 'wasi', 'replica']).default('interpreter'))
215
216
  .addOption(new Option('--replica <replica>', 'Which replica to use to run tests in replica mode').choices(['dfx', 'pocket-ic']))
216
217
  .option('-w, --watch', 'Enable watch mode')
218
+ .option('--verbose', 'Verbose output')
217
219
  .action(async (filter, options) => {
218
220
  checkConfigFile(true);
219
221
  await installAll({silent: true, lock: 'ignore', installFromLockFile: true});
@@ -289,6 +291,68 @@ userCommand
289
291
 
290
292
  program.addCommand(userCommand);
291
293
 
294
+ // mops owner *
295
+ const ownerCommand = new Command('owner').description('Package owner management');
296
+
297
+ // mops owner list
298
+ ownerCommand
299
+ .command('list')
300
+ .description('List package owners')
301
+ .action(async () => {
302
+ await printOwners();
303
+ });
304
+
305
+ // mops owner add
306
+ ownerCommand
307
+ .command('add <principal>')
308
+ .description('Add package owner')
309
+ .addOption(new Option('--yes', 'Do not ask for confirmation'))
310
+ .action(async (data, options) => {
311
+ await addOwner(data, options.yes);
312
+ });
313
+
314
+ // mops owner remove
315
+ ownerCommand
316
+ .command('remove <principal>')
317
+ .description('Remove package owner')
318
+ .addOption(new Option('--yes', 'Do not ask for confirmation'))
319
+ .action(async (data, options) => {
320
+ await removeOwner(data, options.yes);
321
+ });
322
+
323
+ program.addCommand(ownerCommand);
324
+
325
+ // mops maintainer *
326
+ const maintainerCommand = new Command('maintainer').description('Package maintainer management');
327
+
328
+ // mops maintainer list
329
+ maintainerCommand
330
+ .command('list')
331
+ .description('List package maintainers')
332
+ .action(async () => {
333
+ await printMaintainers();
334
+ });
335
+
336
+ // mops maintainer add
337
+ maintainerCommand
338
+ .command('add <principal>')
339
+ .description('Add package maintainer')
340
+ .addOption(new Option('--yes', 'Do not ask for confirmation'))
341
+ .action(async (data, options) => {
342
+ await addMaintainer(data, options.yes);
343
+ });
344
+
345
+ // mops maintainer remove
346
+ maintainerCommand
347
+ .command('remove <principal>')
348
+ .description('Remove package maintainer')
349
+ .addOption(new Option('--yes', 'Do not ask for confirmation'))
350
+ .action(async (data, options) => {
351
+ await removeMaintainer(data, options.yes);
352
+ });
353
+
354
+ program.addCommand(maintainerCommand);
355
+
292
356
  // bump
293
357
  program
294
358
  .command('bump [major|minor|patch]')
@@ -323,14 +387,6 @@ program
323
387
  await update(pkg, options);
324
388
  });
325
389
 
326
- // transfer-ownership
327
- program
328
- .command('transfer-ownership [to-principal]')
329
- .description('Transfer ownership of the current package to another principal')
330
- .action(async (toPrincipal) => {
331
- await transferOwnership(toPrincipal);
332
- });
333
-
334
390
  // toolchain
335
391
  const toolchainCommand = new Command('toolchain').description('Toolchain management');
336
392
 
@@ -1,5 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import path from 'node:path';
3
+ import {existsSync} from 'node:fs';
3
4
  import {createLogUpdate} from 'log-update';
4
5
  import {getRootDir, readConfig} from '../../mops.js';
5
6
  import {installDeps} from './install-deps.js';
@@ -28,10 +29,16 @@ export async function installLocalDep(pkg : string, pkgPath = '', {verbose, sile
28
29
  // install dependencies
29
30
  if (!ignoreTransitive) {
30
31
  let dir = path.resolve(getRootDir(), pkgPath).replaceAll('{MOPS_ENV}', process.env.MOPS_ENV || 'local');
31
- let config = readConfig(path.join(dir, 'mops.toml'));
32
+ let mopsToml = path.join(dir, 'mops.toml');
33
+
34
+ if (!existsSync(mopsToml)) {
35
+ return true;
36
+ }
37
+
38
+ let config = readConfig(mopsToml);
32
39
  return installDeps(Object.values(config.dependencies || {}), {silent, verbose}, pkgPath);
33
40
  }
34
41
  else {
35
42
  return true;
36
43
  }
37
- }
44
+ }
@@ -0,0 +1,105 @@
1
+ import process from 'node:process';
2
+ import chalk from 'chalk';
3
+ import {checkConfigFile, getIdentity, readConfig} from '../mops.js';
4
+ import {mainActor} from '../api/actors.js';
5
+ import {Principal} from '@dfinity/principal';
6
+ import prompts from 'prompts';
7
+
8
+ export async function printMaintainers() {
9
+ if (!checkConfigFile()) {
10
+ return;
11
+ }
12
+
13
+ let config = readConfig();
14
+ let actor = await mainActor();
15
+
16
+ let maintainers = await actor.getPackageMaintainers(config.package?.name || '');
17
+ console.log(`Maintainers of package ${chalk.bold(config.package?.name)}:`);
18
+ for (let maintainer of maintainers) {
19
+ console.log(maintainer.toText());
20
+ }
21
+ }
22
+
23
+ export async function addMaintainer(maintainer : string, yes = false) {
24
+ if (!checkConfigFile()) {
25
+ return;
26
+ }
27
+
28
+ let config = readConfig();
29
+ let principal = Principal.fromText(maintainer);
30
+
31
+ if (!yes) {
32
+ let promptsConfig = {
33
+ onCancel() {
34
+ console.log('aborted');
35
+ process.exit(0);
36
+ },
37
+ };
38
+
39
+ let {confirm} = await prompts({
40
+ type: 'confirm',
41
+ name: 'confirm',
42
+ message: `Are you sure you want to add maintainer ${chalk.yellow(maintainer)} to ${chalk.yellow(config.package?.name)} package?`,
43
+ initial: true,
44
+ }, promptsConfig);
45
+
46
+ if (!confirm) {
47
+ return;
48
+ }
49
+ }
50
+
51
+ let identity = await getIdentity();
52
+ let actor = await mainActor(identity);
53
+
54
+ let res = await actor.addMaintainer(config.package?.name || '', principal);
55
+ if ('ok' in res) {
56
+ console.log(chalk.green('Success!'));
57
+ console.log(`Added maintainer ${chalk.bold(maintainer)} to package ${chalk.bold(config.package?.name)}`);
58
+ }
59
+ else {
60
+ console.error(chalk.red('Error: ') + res.err);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ export async function removeMaintainer(maintainer : string, yes = false) {
66
+ if (!checkConfigFile()) {
67
+ return;
68
+ }
69
+
70
+ let config = readConfig();
71
+ let principal = Principal.fromText(maintainer);
72
+
73
+ if (!yes) {
74
+ let promptsConfig = {
75
+ onCancel() {
76
+ console.log('aborted');
77
+ process.exit(0);
78
+ },
79
+ };
80
+
81
+ let {confirm} = await prompts({
82
+ type: 'confirm',
83
+ name: 'confirm',
84
+ message: `Are you sure you want to remove maintainer ${chalk.red(maintainer)} from ${chalk.red(config.package?.name)} package?`,
85
+ initial: true,
86
+ }, promptsConfig);
87
+
88
+ if (!confirm) {
89
+ return;
90
+ }
91
+ }
92
+
93
+ let identity = await getIdentity();
94
+ let actor = await mainActor(identity);
95
+
96
+ let res = await actor.removeMaintainer(config.package?.name || '', principal);
97
+ if ('ok' in res) {
98
+ console.log(chalk.green('Success!'));
99
+ console.log(`Removed maintainer ${chalk.bold(maintainer)} from package ${chalk.bold(config.package?.name)}`);
100
+ }
101
+ else {
102
+ console.error(chalk.red('Error: ') + res.err);
103
+ process.exit(1);
104
+ }
105
+ }
@@ -0,0 +1,105 @@
1
+ import process from 'node:process';
2
+ import chalk from 'chalk';
3
+ import {checkConfigFile, getIdentity, readConfig} from '../mops.js';
4
+ import {mainActor} from '../api/actors.js';
5
+ import {Principal} from '@dfinity/principal';
6
+ import prompts from 'prompts';
7
+
8
+ export async function printOwners() {
9
+ if (!checkConfigFile()) {
10
+ return;
11
+ }
12
+
13
+ let config = readConfig();
14
+ let actor = await mainActor();
15
+
16
+ let owners = await actor.getPackageOwners(config.package?.name || '');
17
+ console.log(`Owners of package ${chalk.bold(config.package?.name)}:`);
18
+ for (let owner of owners) {
19
+ console.log(owner.toText());
20
+ }
21
+ }
22
+
23
+ export async function addOwner(owner : string, yes = false) {
24
+ if (!checkConfigFile()) {
25
+ return;
26
+ }
27
+
28
+ let config = readConfig();
29
+ let principal = Principal.fromText(owner);
30
+
31
+ if (!yes) {
32
+ let promptsConfig = {
33
+ onCancel() {
34
+ console.log('aborted');
35
+ process.exit(0);
36
+ },
37
+ };
38
+
39
+ let {confirm} = await prompts({
40
+ type: 'confirm',
41
+ name: 'confirm',
42
+ message: `Are you sure you want to add owner ${chalk.yellow(owner)} to ${chalk.yellow(config.package?.name)} package?`,
43
+ initial: true,
44
+ }, promptsConfig);
45
+
46
+ if (!confirm) {
47
+ return;
48
+ }
49
+ }
50
+
51
+ let identity = await getIdentity();
52
+ let actor = await mainActor(identity);
53
+
54
+ let res = await actor.addOwner(config.package?.name || '', principal);
55
+ if ('ok' in res) {
56
+ console.log(chalk.green('Success!'));
57
+ console.log(`Added owner ${chalk.bold(owner)} to package ${chalk.bold(config.package?.name)}`);
58
+ }
59
+ else {
60
+ console.error(chalk.red('Error: ') + res.err);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ export async function removeOwner(owner : string, yes = false) {
66
+ if (!checkConfigFile()) {
67
+ return;
68
+ }
69
+
70
+ let config = readConfig();
71
+ let principal = Principal.fromText(owner);
72
+
73
+ if (!yes) {
74
+ let promptsConfig = {
75
+ onCancel() {
76
+ console.log('aborted');
77
+ process.exit(0);
78
+ },
79
+ };
80
+
81
+ let {confirm} = await prompts({
82
+ type: 'confirm',
83
+ name: 'confirm',
84
+ message: `Are you sure you want to remove owner ${chalk.red(owner)} from ${chalk.red(config.package?.name)} package?`,
85
+ initial: true,
86
+ }, promptsConfig);
87
+
88
+ if (!confirm) {
89
+ return;
90
+ }
91
+ }
92
+
93
+ let identity = await getIdentity();
94
+ let actor = await mainActor(identity);
95
+
96
+ let res = await actor.removeOwner(config.package?.name || '', principal);
97
+ if ('ok' in res) {
98
+ console.log(chalk.green('Success!'));
99
+ console.log(`Removed owner ${chalk.bold(owner)} from package ${chalk.bold(config.package?.name)}`);
100
+ }
101
+ else {
102
+ console.error(chalk.red('Error: ') + res.err);
103
+ process.exit(1);
104
+ }
105
+ }
@@ -8,19 +8,21 @@ import {spawn as spawnAsync} from 'promisify-child-process';
8
8
  import {IDL} from '@dfinity/candid';
9
9
  import {Actor, HttpAgent} from '@dfinity/agent';
10
10
  import {PocketIc, PocketIcServer} from 'pic-ic';
11
+ import chalk from 'chalk';
11
12
 
12
13
  import {readConfig} from '../mops.js';
13
14
  import {toolchain} from './toolchain/index.js';
15
+ import {getDfxVersion} from '../helpers/get-dfx-version.js';
14
16
 
15
17
  type StartOptions = {
16
- type ?: 'dfx' | 'pocket-ic';
18
+ type ?: 'dfx' | 'pocket-ic' | 'dfx-pocket-ic';
17
19
  dir ?: string;
18
20
  verbose ?: boolean;
19
21
  silent ?: boolean;
20
22
  };
21
23
 
22
24
  export class Replica {
23
- type : 'dfx' | 'pocket-ic' = 'dfx';
25
+ type : 'dfx' | 'pocket-ic' | 'dfx-pocket-ic' = 'dfx';
24
26
  verbose = false;
25
27
  canisters : Record<string, {cwd : string; canisterId : string; actor : any; stream : PassThrough;}> = {};
26
28
  pocketIcServer ?: PocketIcServer;
@@ -36,20 +38,33 @@ export class Replica {
36
38
 
37
39
  silent || console.log(`Starting ${this.type} replica...`);
38
40
 
39
- if (this.type == 'dfx') {
41
+ if (this.type === 'dfx' || this.type === 'dfx-pocket-ic') {
40
42
  fs.mkdirSync(this.dir, {recursive: true});
41
43
  fs.writeFileSync(path.join(this.dir, 'dfx.json'), JSON.stringify(this.dfxJson(''), null, 2));
42
44
  fs.writeFileSync(path.join(this.dir, 'canister.did'), 'service : { runTests: () -> (); }');
43
45
 
44
46
  await this.stop();
45
47
 
46
- this.dfxProcess = spawn('dfx', ['start', '--clean', '--artificial-delay', '0', (this.verbose ? '' : '-qqqq')].filter(x => x), {cwd: this.dir});
48
+ this.dfxProcess = spawn('dfx', ['start', this.type === 'dfx-pocket-ic' ? '--pocketic' : '', '--clean', (this.verbose ? '' : '-qqqq'), '--artificial-delay', '0'].filter(x => x).flat(), {cwd: this.dir});
47
49
 
48
50
  // process canister logs
49
51
  this._attachCanisterLogHandler(this.dfxProcess);
50
52
 
51
53
  this.dfxProcess.stdout.on('data', (data) => {
52
- console.log('DFX:', data.toString());
54
+ if (this.verbose) {
55
+ console.log('DFX:', data.toString());
56
+ }
57
+ });
58
+
59
+ this.dfxProcess.stderr.on('data', (data) => {
60
+ if (this.verbose) {
61
+ console.error('DFX:', data.toString());
62
+ }
63
+ if (data.toString().includes('Failed to bind socket to')) {
64
+ console.error(chalk.red(data.toString()));
65
+ console.log('Please try again after some time');
66
+ process.exit(11);
67
+ }
53
68
  });
54
69
 
55
70
  // await for dfx to start
@@ -115,9 +130,22 @@ export class Replica {
115
130
  }
116
131
 
117
132
  async stop(sigint = false) {
118
- if (this.type == 'dfx') {
119
- this.dfxProcess?.kill();
120
- // execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, timeout: 10_000, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']});
133
+ if (this.type === 'dfx' || this.type === 'dfx-pocket-ic') {
134
+ if (this.dfxProcess) {
135
+ this.dfxProcess.kill();
136
+ // give replica some time to stop
137
+ await new Promise((resolve) => {
138
+ setTimeout(resolve, 1000);
139
+ });
140
+ }
141
+
142
+ // if (!this.dfxProcess) {
143
+ // try {
144
+ // execSync('dfx killall', {cwd: this.dir, timeout: 3_000, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']});
145
+ // execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, timeout: 10_000, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']});
146
+ // }
147
+ // catch {}
148
+ // }
121
149
  }
122
150
  else if (this.pocketIc && this.pocketIcServer) {
123
151
  if (!sigint) {
@@ -128,7 +156,7 @@ export class Replica {
128
156
  }
129
157
 
130
158
  async deploy(name : string, wasm : string, idlFactory : IDL.InterfaceFactory, cwd : string = process.cwd(), signal ?: AbortSignal) {
131
- if (this.type === 'dfx') {
159
+ if (this.type === 'dfx' || this.type === 'dfx-pocket-ic') {
132
160
  // prepare dfx.json for current canister
133
161
  let dfxJson = path.join(this.dir, 'dfx.json');
134
162
 
@@ -253,6 +281,7 @@ export class Replica {
253
281
  return {
254
282
  version: 1,
255
283
  canisters,
284
+ dfx: getDfxVersion(),
256
285
  defaults: {
257
286
  build: {
258
287
  packtool: 'mops sources',
@@ -8,6 +8,7 @@ import chalk from 'chalk';
8
8
  import {globSync} from 'glob';
9
9
  import chokidar from 'chokidar';
10
10
  import debounce from 'debounce';
11
+ import {SemVer} from 'semver';
11
12
 
12
13
  import {sources} from '../sources.js';
13
14
  import {getRootDir, readConfig} from '../../mops.js';
@@ -25,6 +26,7 @@ import {Replica} from '../replica.js';
25
26
  import {ActorMethod} from '@dfinity/agent';
26
27
  import {PassThrough, Readable} from 'node:stream';
27
28
  import {TestMode} from '../../types.js';
29
+ import {getDfxVersion} from '../../helpers/get-dfx-version.js';
28
30
 
29
31
  let ignore = [
30
32
  '**/node_modules/**',
@@ -39,13 +41,14 @@ let globConfig = {
39
41
  };
40
42
 
41
43
  type ReporterName = 'verbose' | 'files' | 'compact' | 'silent';
42
- type ReplicaName = 'dfx' | 'pocket-ic';
44
+ type ReplicaName = 'dfx' | 'pocket-ic' | 'dfx-pocket-ic';
43
45
 
44
46
  type TestOptions = {
45
47
  watch : boolean;
46
48
  reporter : ReporterName;
47
49
  mode : TestMode;
48
50
  replica : ReplicaName;
51
+ verbose : boolean;
49
52
  };
50
53
 
51
54
 
@@ -66,7 +69,20 @@ export async function test(filter = '', options : Partial<TestOptions> = {}) {
66
69
  let rootDir = getRootDir();
67
70
 
68
71
  let replicaType = options.replica ?? (config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx' as ReplicaName);
72
+
73
+ if (replicaType === 'pocket-ic' && !config.toolchain?.['pocket-ic']) {
74
+ let dfxVersion = getDfxVersion();
75
+ if (!dfxVersion || new SemVer(dfxVersion).compare('0.24.1') < 0) {
76
+ console.log(chalk.red('Please update dfx to the version >=0.24.1 or specify pocket-ic version in mops.toml'));
77
+ process.exit(1);
78
+ }
79
+ else {
80
+ replicaType = 'dfx-pocket-ic';
81
+ }
82
+ }
83
+
69
84
  replica.type = replicaType;
85
+ replica.verbose = !!options.verbose;
70
86
 
71
87
  if (options.watch) {
72
88
  replica.ttl = 60 * 15; // 15 minutes
@@ -202,6 +218,7 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
202
218
  let testTempDir = path.join(getRootDir(), '.mops/.test/');
203
219
  replica.dir = testTempDir;
204
220
 
221
+ fs.rmSync(testTempDir, {recursive: true, force: true});
205
222
  fs.mkdirSync(testTempDir, {recursive: true});
206
223
 
207
224
  await parallel(os.cpus().length, files, async (file : string) => {
@@ -51,14 +51,14 @@ function readDfxJson() : DfxConfig | Record<string, never> {
51
51
 
52
52
  export function getMotokoCanisters() : Record<string, string> {
53
53
  let dfxJson = readDfxJson();
54
- return Object.fromEntries(Object.entries(dfxJson.canisters)
54
+ return Object.fromEntries(Object.entries(dfxJson.canisters || {})
55
55
  .filter(([_, canister]) => canister.type === 'motoko')
56
56
  .map(([name, canister]) => [name, canister.main ?? '']));
57
57
  }
58
58
 
59
59
  export function getMotokoCanistersWithDeclarations() : Record<string, string> {
60
60
  let dfxJson = readDfxJson();
61
- return Object.fromEntries(Object.entries(dfxJson.canisters)
61
+ return Object.fromEntries(Object.entries(dfxJson.canisters || {})
62
62
  .filter(([_, canister]) => canister.type === 'motoko' && canister.declarations)
63
63
  .map(([name, canister]) => [name, canister.main ?? '']));
64
64
  }