mobilestacks 0.1.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.
Files changed (96) hide show
  1. package/Clarinet.toml +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +188 -0
  4. package/contracts/sample-contract.clar +2 -0
  5. package/dist/mobilestacks.config.d.ts +20 -0
  6. package/dist/mobilestacks.config.d.ts.map +1 -0
  7. package/dist/mobilestacks.config.js +23 -0
  8. package/dist/src/cli/index.d.ts +3 -0
  9. package/dist/src/cli/index.d.ts.map +1 -0
  10. package/dist/src/cli/index.js +102 -0
  11. package/dist/src/cli/init.d.ts +2 -0
  12. package/dist/src/cli/init.d.ts.map +1 -0
  13. package/dist/src/cli/init.js +55 -0
  14. package/dist/src/config/config-loading.d.ts +3 -0
  15. package/dist/src/config/config-loading.d.ts.map +1 -0
  16. package/dist/src/config/config-loading.js +29 -0
  17. package/dist/src/core/dsl.d.ts +30 -0
  18. package/dist/src/core/dsl.d.ts.map +1 -0
  19. package/dist/src/core/dsl.js +62 -0
  20. package/dist/src/core/dsl.test.d.ts +2 -0
  21. package/dist/src/core/dsl.test.d.ts.map +1 -0
  22. package/dist/src/core/dsl.test.js +48 -0
  23. package/dist/src/core/env.d.ts +6 -0
  24. package/dist/src/core/env.d.ts.map +1 -0
  25. package/dist/src/core/env.js +7 -0
  26. package/dist/src/core/extender.d.ts +12 -0
  27. package/dist/src/core/extender.d.ts.map +1 -0
  28. package/dist/src/core/extender.js +20 -0
  29. package/dist/src/core/runtime-environment.d.ts +15 -0
  30. package/dist/src/core/runtime-environment.d.ts.map +1 -0
  31. package/dist/src/core/runtime-environment.js +65 -0
  32. package/dist/src/core/simnet.d.ts +17 -0
  33. package/dist/src/core/simnet.d.ts.map +1 -0
  34. package/dist/src/core/simnet.js +42 -0
  35. package/dist/src/core/tasks-definitions.d.ts +28 -0
  36. package/dist/src/core/tasks-definitions.d.ts.map +1 -0
  37. package/dist/src/core/tasks-definitions.js +21 -0
  38. package/dist/src/index.d.ts +5 -0
  39. package/dist/src/index.d.ts.map +1 -0
  40. package/dist/src/index.js +4 -0
  41. package/dist/src/tasks/call-contract-function.d.ts +2 -0
  42. package/dist/src/tasks/call-contract-function.d.ts.map +1 -0
  43. package/dist/src/tasks/call-contract-function.js +31 -0
  44. package/dist/src/tasks/deploy-contract.d.ts +2 -0
  45. package/dist/src/tasks/deploy-contract.d.ts.map +1 -0
  46. package/dist/src/tasks/deploy-contract.js +44 -0
  47. package/dist/src/tasks/example-task.d.ts +2 -0
  48. package/dist/src/tasks/example-task.d.ts.map +1 -0
  49. package/dist/src/tasks/example-task.js +6 -0
  50. package/dist/src/tasks/faucet-request.d.ts +2 -0
  51. package/dist/src/tasks/faucet-request.d.ts.map +1 -0
  52. package/dist/src/tasks/faucet-request.js +21 -0
  53. package/dist/src/tasks/get-balance.d.ts +2 -0
  54. package/dist/src/tasks/get-balance.d.ts.map +1 -0
  55. package/dist/src/tasks/get-balance.js +33 -0
  56. package/dist/src/tasks/get-contract-info.d.ts +2 -0
  57. package/dist/src/tasks/get-contract-info.d.ts.map +1 -0
  58. package/dist/src/tasks/get-contract-info.js +18 -0
  59. package/dist/src/tasks/get-tx-history.d.ts +2 -0
  60. package/dist/src/tasks/get-tx-history.d.ts.map +1 -0
  61. package/dist/src/tasks/get-tx-history.js +38 -0
  62. package/dist/src/tasks/list-accounts.d.ts +2 -0
  63. package/dist/src/tasks/list-accounts.d.ts.map +1 -0
  64. package/dist/src/tasks/list-accounts.js +24 -0
  65. package/dist/src/tasks/send-stx.d.ts +2 -0
  66. package/dist/src/tasks/send-stx.d.ts.map +1 -0
  67. package/dist/src/tasks/send-stx.js +36 -0
  68. package/dist/src/tasks/verify-contract.d.ts +2 -0
  69. package/dist/src/tasks/verify-contract.d.ts.map +1 -0
  70. package/dist/src/tasks/verify-contract.js +33 -0
  71. package/dist/src/types/config.d.ts +120 -0
  72. package/dist/src/types/config.d.ts.map +1 -0
  73. package/dist/src/types/config.js +19 -0
  74. package/package.json +70 -0
  75. package/src/cli/index.ts +105 -0
  76. package/src/cli/init.ts +57 -0
  77. package/src/config/config-loading.ts +31 -0
  78. package/src/core/dsl.test.ts +63 -0
  79. package/src/core/dsl.ts +73 -0
  80. package/src/core/env.ts +8 -0
  81. package/src/core/extender.ts +29 -0
  82. package/src/core/runtime-environment.ts +74 -0
  83. package/src/core/simnet.ts +56 -0
  84. package/src/core/tasks-definitions.ts +47 -0
  85. package/src/index.ts +4 -0
  86. package/src/tasks/call-contract-function.ts +31 -0
  87. package/src/tasks/deploy-contract.ts +49 -0
  88. package/src/tasks/example-task.ts +7 -0
  89. package/src/tasks/faucet-request.ts +21 -0
  90. package/src/tasks/get-balance.ts +34 -0
  91. package/src/tasks/get-contract-info.ts +18 -0
  92. package/src/tasks/get-tx-history.ts +39 -0
  93. package/src/tasks/list-accounts.ts +27 -0
  94. package/src/tasks/send-stx.ts +39 -0
  95. package/src/tasks/verify-contract.ts +33 -0
  96. package/src/types/config.ts +30 -0
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+ export declare const NetworkConfigSchema: z.ZodObject<{
3
+ url: z.ZodString;
4
+ name: z.ZodString;
5
+ explorerUrl: z.ZodOptional<z.ZodString>;
6
+ faucetUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ url: string;
9
+ name: string;
10
+ explorerUrl?: string | undefined;
11
+ faucetUrl?: string | null | undefined;
12
+ }, {
13
+ url: string;
14
+ name: string;
15
+ explorerUrl?: string | undefined;
16
+ faucetUrl?: string | null | undefined;
17
+ }>;
18
+ export declare const WalletConfigSchema: z.ZodEffects<z.ZodObject<{
19
+ privateKey: z.ZodOptional<z.ZodString>;
20
+ seedPhrase: z.ZodOptional<z.ZodString>;
21
+ derivationPath: z.ZodOptional<z.ZodString>;
22
+ address: z.ZodOptional<z.ZodString>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ privateKey?: string | undefined;
25
+ seedPhrase?: string | undefined;
26
+ derivationPath?: string | undefined;
27
+ address?: string | undefined;
28
+ }, {
29
+ privateKey?: string | undefined;
30
+ seedPhrase?: string | undefined;
31
+ derivationPath?: string | undefined;
32
+ address?: string | undefined;
33
+ }>, {
34
+ privateKey?: string | undefined;
35
+ seedPhrase?: string | undefined;
36
+ derivationPath?: string | undefined;
37
+ address?: string | undefined;
38
+ }, {
39
+ privateKey?: string | undefined;
40
+ seedPhrase?: string | undefined;
41
+ derivationPath?: string | undefined;
42
+ address?: string | undefined;
43
+ }>;
44
+ export declare const MobilestacksConfigSchema: z.ZodObject<{
45
+ networks: z.ZodRecord<z.ZodString, z.ZodObject<{
46
+ url: z.ZodString;
47
+ name: z.ZodString;
48
+ explorerUrl: z.ZodOptional<z.ZodString>;
49
+ faucetUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
50
+ }, "strip", z.ZodTypeAny, {
51
+ url: string;
52
+ name: string;
53
+ explorerUrl?: string | undefined;
54
+ faucetUrl?: string | null | undefined;
55
+ }, {
56
+ url: string;
57
+ name: string;
58
+ explorerUrl?: string | undefined;
59
+ faucetUrl?: string | null | undefined;
60
+ }>>;
61
+ defaultNetwork: z.ZodString;
62
+ wallet: z.ZodEffects<z.ZodObject<{
63
+ privateKey: z.ZodOptional<z.ZodString>;
64
+ seedPhrase: z.ZodOptional<z.ZodString>;
65
+ derivationPath: z.ZodOptional<z.ZodString>;
66
+ address: z.ZodOptional<z.ZodString>;
67
+ }, "strip", z.ZodTypeAny, {
68
+ privateKey?: string | undefined;
69
+ seedPhrase?: string | undefined;
70
+ derivationPath?: string | undefined;
71
+ address?: string | undefined;
72
+ }, {
73
+ privateKey?: string | undefined;
74
+ seedPhrase?: string | undefined;
75
+ derivationPath?: string | undefined;
76
+ address?: string | undefined;
77
+ }>, {
78
+ privateKey?: string | undefined;
79
+ seedPhrase?: string | undefined;
80
+ derivationPath?: string | undefined;
81
+ address?: string | undefined;
82
+ }, {
83
+ privateKey?: string | undefined;
84
+ seedPhrase?: string | undefined;
85
+ derivationPath?: string | undefined;
86
+ address?: string | undefined;
87
+ }>;
88
+ }, "strip", z.ZodTypeAny, {
89
+ networks: Record<string, {
90
+ url: string;
91
+ name: string;
92
+ explorerUrl?: string | undefined;
93
+ faucetUrl?: string | null | undefined;
94
+ }>;
95
+ defaultNetwork: string;
96
+ wallet: {
97
+ privateKey?: string | undefined;
98
+ seedPhrase?: string | undefined;
99
+ derivationPath?: string | undefined;
100
+ address?: string | undefined;
101
+ };
102
+ }, {
103
+ networks: Record<string, {
104
+ url: string;
105
+ name: string;
106
+ explorerUrl?: string | undefined;
107
+ faucetUrl?: string | null | undefined;
108
+ }>;
109
+ defaultNetwork: string;
110
+ wallet: {
111
+ privateKey?: string | undefined;
112
+ seedPhrase?: string | undefined;
113
+ derivationPath?: string | undefined;
114
+ address?: string | undefined;
115
+ };
116
+ }>;
117
+ export type NetworkConfig = z.infer<typeof NetworkConfigSchema>;
118
+ export type WalletConfig = z.infer<typeof WalletConfigSchema>;
119
+ export type MobilestacksConfig = z.infer<typeof MobilestacksConfigSchema>;
120
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;EAK9B,CAAC;AAIH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;EAQ9B,CAAC;AAEF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAInC,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ export const NetworkConfigSchema = z.object({
3
+ url: z.string().url(),
4
+ name: z.string(),
5
+ explorerUrl: z.string().url().optional(),
6
+ faucetUrl: z.string().url().nullable().optional(),
7
+ });
8
+ // Wallet config: allow privateKey, seedPhrase, or both. If both, privateKey is used.
9
+ export const WalletConfigSchema = z.object({
10
+ privateKey: z.string().min(1, "Private key is required").optional(),
11
+ seedPhrase: z.string().optional(),
12
+ derivationPath: z.string().optional(),
13
+ address: z.string().optional(),
14
+ }).refine((data) => data.privateKey || data.seedPhrase, { message: "Either privateKey or seedPhrase is required" });
15
+ export const MobilestacksConfigSchema = z.object({
16
+ networks: z.record(NetworkConfigSchema),
17
+ defaultNetwork: z.string(),
18
+ wallet: WalletConfigSchema,
19
+ });
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "mobilestacks",
3
+ "version": "0.1.0",
4
+ "description": "Professional Task Runner & CLI for the Stacks Blockchain",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "mobilestacks": "dist/cli/index.js"
9
+ },
10
+ "type": "module",
11
+ "files": [
12
+ "dist/",
13
+ "src/",
14
+ "contracts/",
15
+ "Clarinet.toml",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "start": "node ./dist/cli/index.js",
21
+ "build": "tsc",
22
+ "lint": "eslint src mobilestacks.config.ts --ext .ts",
23
+ "test": "vitest run",
24
+ "setup:simnet": "node scripts/setup-simnet.mjs",
25
+ "prepublishOnly": "npm run build && npm test"
26
+ },
27
+ "keywords": [
28
+ "stacks",
29
+ "blockchain",
30
+ "clarity",
31
+ "smart-contracts",
32
+ "cli",
33
+ "task-runner",
34
+ "web3",
35
+ "bitcoin"
36
+ ],
37
+ "author": "mobilestacks",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/mobilestacks/mobilestacks.git"
42
+ },
43
+ "homepage": "https://github.com/mobilestacks/mobilestacks#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/mobilestacks/mobilestacks/issues"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "devDependencies": {
51
+ "@hirosystems/clarinet-sdk": "^3.8.1",
52
+ "@types/inquirer": "^8.2.6",
53
+ "@typescript-eslint/eslint-plugin": "^6.20.0",
54
+ "@typescript-eslint/parser": "^6.20.0",
55
+ "eslint": "^8.56.0",
56
+ "eslint-config-prettier": "^10.1.8",
57
+ "eslint-import-resolver-typescript": "^4.4.4",
58
+ "eslint-plugin-import": "^2.32.0",
59
+ "vitest": "^4.0.18"
60
+ },
61
+ "dependencies": {
62
+ "@stacks/network": "latest",
63
+ "@stacks/transactions": "latest",
64
+ "@stacks/wallet-sdk": "latest",
65
+ "commander": "^11.0.0",
66
+ "node-fetch": "^3.3.2",
67
+ "ts-node": "^10.9.1",
68
+ "zod": "^3.25.76"
69
+ }
70
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import chalk from 'chalk';
3
+ import { Command } from 'commander';
4
+ import { loadConfig } from '../config/config-loading';
5
+ import { RuntimeEnvironment } from '../core/runtime-environment';
6
+ import { TaskDefinitions } from '../core/tasks-definitions';
7
+ import { runInit } from './init';
8
+ import inquirer from 'inquirer';
9
+
10
+ // Import all user tasks
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ // Auto-load all tasks in src/tasks
14
+ const tasksDir = path.join(__dirname, '../tasks');
15
+ fs.readdirSync(tasksDir)
16
+ .filter(f => f.endsWith('.ts') || f.endsWith('.js'))
17
+ .forEach(f => {
18
+ require(path.join(tasksDir, f));
19
+ });
20
+
21
+ const program = new Command();
22
+ program
23
+ .name('mobilestacks')
24
+ .description('Professional Task Runner for Stacks')
25
+ .version('0.1.0');
26
+
27
+
28
+ // Init command for project scaffolding
29
+ program
30
+ .command('init')
31
+ .description('Scaffold a new mobilestacks project and config')
32
+ .action(async () => {
33
+ await runInit();
34
+ process.exit(0);
35
+ });
36
+
37
+ // List all tasks if no command is given
38
+ program.action(() => {
39
+ console.log(chalk.bold.blue('\nMobilestacks - Professional Task Runner for Stacks\n'));
40
+ console.log(chalk.white('USAGE: ') + chalk.green('mobilestacks <task> [options]\n'));
41
+ console.log(chalk.bold('Available tasks:'));
42
+ TaskDefinitions.getInstance().getAllTasks().forEach(task => {
43
+ const params = task.params.map(p => chalk.yellow(`--${p.name}`)).join(' ');
44
+ console.log(' ' + chalk.cyan(task.name) + ' ' + params);
45
+ console.log(' ' + chalk.gray(task.description));
46
+ });
47
+ console.log('\n' + chalk.white('Use ') + chalk.green('mobilestacks <task> --help') + chalk.white(' for more info on a task.'));
48
+ console.log(chalk.white('\nExample:'));
49
+ console.log(' ' + chalk.green('mobilestacks deploy-contract --contractName my-contract --file ./contracts/my-contract.clar --network testnet'));
50
+ console.log(chalk.white('\nDocs: ') + chalk.underline('https://github.com/your-org/mobilestacks#readme'));
51
+ program.help({ error: false });
52
+ });
53
+
54
+ // Dynamically add all registered tasks
55
+
56
+ TaskDefinitions.getInstance().getAllTasks().forEach(task => {
57
+ const cmd = program.command(task.name)
58
+ .description(task.description);
59
+ task.params.forEach(param => {
60
+ const optStr = `--${param.name} <value>`;
61
+ if (param.required !== false) {
62
+ cmd.option(optStr, param.description);
63
+ } else {
64
+ cmd.option(optStr, param.description, param.defaultValue as string | boolean | undefined);
65
+ }
66
+ });
67
+ cmd.action(async (opts) => {
68
+ try {
69
+ // Prompt for missing required params
70
+ const missing = task.params.filter(p => p.required !== false && !opts[p.name]);
71
+ if (missing.length > 0) {
72
+ const answers = await inquirer.prompt(missing.map(p => ({
73
+ type: p.type === 'boolean' ? 'confirm' : 'input',
74
+ name: p.name,
75
+ message: p.description,
76
+ default: p.defaultValue
77
+ })));
78
+ Object.assign(opts, answers);
79
+ }
80
+ // Type conversion
81
+ task.params.forEach(p => {
82
+ if (opts[p.name] && p.type === 'number') opts[p.name] = Number(opts[p.name]);
83
+ if (opts[p.name] && p.type === 'boolean') opts[p.name] = Boolean(opts[p.name]);
84
+ });
85
+ const config = loadConfig();
86
+ const env = new RuntimeEnvironment(config);
87
+ const result = await task.action(opts, env);
88
+ if (typeof result === 'object') {
89
+ console.log(chalk.greenBright('Success!'));
90
+ console.dir(result, { depth: null, colors: true });
91
+ } else {
92
+ console.log(chalk.greenBright(result));
93
+ }
94
+ } catch (err) {
95
+ const error = err as Error;
96
+ console.error(chalk.redBright('Task failed:'), error.message || error);
97
+ if (error && typeof error === 'object' && 'stack' in error && error.stack) {
98
+ console.error(chalk.gray(error.stack));
99
+ }
100
+ process.exit(1);
101
+ }
102
+ });
103
+ });
104
+
105
+ program.parse(process.argv);
@@ -0,0 +1,57 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import inquirer from 'inquirer';
4
+
5
+ export async function runInit() {
6
+ console.log('Welcome to mobilestacks project initialization!');
7
+ const answers = await inquirer.prompt([
8
+ {
9
+ type: 'input',
10
+ name: 'projectName',
11
+ message: 'Project name:',
12
+ default: path.basename(process.cwd()),
13
+ },
14
+ {
15
+ type: 'input',
16
+ name: 'mainnetUrl',
17
+ message: 'Stacks mainnet node URL:',
18
+ default: 'https://stacks-node-api.mainnet.stacks.co',
19
+ },
20
+ {
21
+ type: 'input',
22
+ name: 'testnetUrl',
23
+ message: 'Stacks testnet node URL:',
24
+ default: 'https://stacks-node-api.testnet.stacks.co',
25
+ },
26
+ {
27
+ type: 'input',
28
+ name: 'privateKey',
29
+ message: 'Your wallet private key (leave blank to use seed phrase):',
30
+ },
31
+ {
32
+ type: 'input',
33
+ name: 'seedPhrase',
34
+ message: 'Your wallet seed phrase (leave blank if using private key):',
35
+ },
36
+ {
37
+ type: 'input',
38
+ name: 'derivationPath',
39
+ message: 'Derivation path (default: m/44\'/5757\'/0\'/0/0):',
40
+ default: "m/44'/5757'/0'/0/0",
41
+ },
42
+ ]);
43
+
44
+ const config = `export default {\n networks: {\n mainnet: { url: '${answers.mainnetUrl}', name: 'mainnet' },\n testnet: { url: '${answers.testnetUrl}', name: 'testnet' }\n },\n defaultNetwork: 'testnet',\n wallet: {\n ${answers.privateKey ? `privateKey: '${answers.privateKey}',` : ''}\n ${answers.seedPhrase ? `seedPhrase: '${answers.seedPhrase}',` : ''}\n derivationPath: '${answers.derivationPath}'\n }\n};\n`;
45
+
46
+ fs.writeFileSync(path.join(process.cwd(), 'mobilestacks.config.ts'), config);
47
+ // Scaffold example contract
48
+ const contractsDir = path.join(process.cwd(), 'contracts');
49
+ if (!fs.existsSync(contractsDir)) fs.mkdirSync(contractsDir);
50
+ fs.writeFileSync(path.join(contractsDir, 'sample-contract.clar'), '(define-public (hello-world)\n (ok "Hello, Stacks!"))\n');
51
+ // Scaffold example user task
52
+ const tasksDir = path.join(process.cwd(), 'src', 'tasks');
53
+ if (!fs.existsSync(tasksDir)) fs.mkdirSync(tasksDir, { recursive: true });
54
+ fs.writeFileSync(path.join(tasksDir, 'example-task.ts'),
55
+ "import { task } from '../core/dsl';\n\ntask('example', 'An example user task for onboarding')\n .addParam('name', 'Your name', { type: 'string', required: false, defaultValue: 'World' })\n .setAction(async (args) => {\n return `Hello, ${args.name}! Welcome to mobilestacks.`;\n });\n");
56
+ console.log('Created mobilestacks.config.ts, contracts/sample-contract.clar, and src/tasks/example-task.ts!');
57
+ }
@@ -0,0 +1,31 @@
1
+ import { MobilestacksConfigSchema, MobilestacksConfig } from '../types/config';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { env } from '../core/env';
5
+
6
+ export function loadConfig(configPath?: string, cliOverrides: Record<string, unknown> = {}): MobilestacksConfig {
7
+ const configFile = configPath || path.resolve(process.cwd(), 'mobilestacks.config.ts');
8
+ if (!fs.existsSync(configFile)) {
9
+ throw new Error(`Config file not found: ${configFile}`);
10
+ }
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
13
+ require('ts-node').register();
14
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
15
+ const userConfig = require(configFile);
16
+ let config = userConfig.default || userConfig;
17
+
18
+ // .env override
19
+ if (env.privateKey) config.wallet.privateKey = env.privateKey;
20
+ if (env.mainnetUrl) config.networks.mainnet.url = env.mainnetUrl;
21
+ if (env.testnetUrl) config.networks.testnet.url = env.testnetUrl;
22
+
23
+ // CLI overrides
24
+ config = { ...config, ...cliOverrides };
25
+
26
+ const parsed = MobilestacksConfigSchema.safeParse(config);
27
+ if (!parsed.success) {
28
+ throw new Error('Invalid mobilestacks.config.ts: ' + JSON.stringify(parsed.error.format(), null, 2));
29
+ }
30
+ return parsed.data;
31
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { task } from './dsl';
3
+ import { TaskDefinitions } from './tasks-definitions';
4
+ import { z } from 'zod';
5
+ import { RuntimeEnvironment } from './runtime-environment';
6
+ import { extendEnvironment, Extender } from './extender';
7
+
8
+ describe('DSL', () => {
9
+ beforeEach(() => {
10
+ // Clear tasks before each test
11
+ (TaskDefinitions.getInstance() as unknown as { _tasks: unknown[] })._tasks = [];
12
+ });
13
+
14
+ it('should register a task with generic parameters', () => {
15
+ task('test-task', 'A test task')
16
+ .addParam('p1', 'param 1')
17
+ .setAction(async () => { return 'done'; });
18
+
19
+ const def = TaskDefinitions.getInstance().getTask('test-task');
20
+ expect(def).toBeDefined();
21
+ expect(def?.name).toBe('test-task');
22
+ expect(def?.params.length).toBe(1);
23
+ });
24
+
25
+ it('should validate Zod schema', async () => {
26
+ task('zod-task', 'Task with validation')
27
+ .addParam('age', 'Age param', { schema: z.number().min(18) })
28
+ .setAction(async (args) => {
29
+ return args.age;
30
+ });
31
+
32
+ const def = TaskDefinitions.getInstance().getTask('zod-task');
33
+ expect(def).toBeDefined();
34
+
35
+ // Mock environment
36
+ const env = {} as RuntimeEnvironment;
37
+
38
+ // Valid call
39
+ const result = await def?.action({ age: 20 }, env);
40
+ expect(result).toBe(20);
41
+
42
+ // Invalid call
43
+ await expect(def?.action({ age: 10 }, env)).rejects.toThrow('Invalid value for parameter \'age\'');
44
+ });
45
+
46
+ it('should support environment extensions', () => {
47
+
48
+
49
+ // Clear extensions
50
+ (Extender.getInstance() as unknown as { _extensions: unknown[] })._extensions = [];
51
+
52
+ extendEnvironment((env: RuntimeEnvironment) => {
53
+ env['foo'] = 'bar';
54
+ });
55
+
56
+ const extensions = Extender.getInstance().getExtensions();
57
+ expect(extensions.length).toBe(1);
58
+
59
+ const mockEnv = {} as RuntimeEnvironment;
60
+ extensions[0](mockEnv);
61
+ expect(mockEnv['foo']).toBe('bar');
62
+ });
63
+ });
@@ -0,0 +1,73 @@
1
+ import { TaskDefinitions, TaskDefinition, TaskParam, TaskParamType } from './tasks-definitions';
2
+ import { ZodSchema } from 'zod';
3
+ import { RuntimeEnvironment } from './runtime-environment';
4
+ import { extendEnvironment } from './extender';
5
+
6
+ function task(name: string, description: string) {
7
+ const params: TaskParam[] = [];
8
+ return {
9
+ addParam(
10
+ paramName: string,
11
+ paramDesc: string,
12
+ options?: { type?: TaskParamType; required?: boolean; defaultValue?: unknown; schema?: ZodSchema }
13
+ ) {
14
+ params.push({
15
+ name: paramName,
16
+ description: paramDesc,
17
+ type: options?.type || 'string',
18
+ required: options?.required !== false, // default true
19
+ defaultValue: options?.defaultValue,
20
+ schema: options?.schema,
21
+ });
22
+ return this;
23
+ },
24
+ setAction(action: (args: Record<string, unknown>, env: RuntimeEnvironment) => Promise<unknown>) {
25
+ const def: TaskDefinition = {
26
+ name,
27
+ description,
28
+ params,
29
+ action: async (args: Record<string, unknown>, env: RuntimeEnvironment) => {
30
+ // Validate args against schemas if present
31
+ for (const param of params) {
32
+ if (param.schema && args[param.name] !== undefined) {
33
+ try {
34
+ param.schema.parse(args[param.name]);
35
+ } catch (error) {
36
+ throw new Error(`Invalid value for parameter '${param.name}': ${error}`);
37
+ }
38
+ }
39
+ }
40
+ return action(args, env);
41
+ },
42
+ };
43
+ TaskDefinitions.getInstance().addTask(def);
44
+ return def;
45
+ },
46
+ };
47
+ }
48
+
49
+ function subtask(name: string, description: string, parentTaskName?: string) {
50
+ // Register as a subtask with a parent if provided
51
+ const sub = task(name, description);
52
+ if (parentTaskName) {
53
+ // Attach parent info for future use (e.g., dependency graph, CLI grouping)
54
+ const def = TaskDefinitions.getInstance().getTask(name);
55
+ if (def) (def as TaskDefinition & { parent?: string }).parent = parentTaskName;
56
+ }
57
+ return sub;
58
+ }
59
+
60
+ // Workflow system: define and run ordered task/subtask sequences
61
+ export type WorkflowStep = { taskName: string; args?: Record<string, unknown> };
62
+ export type Workflow = WorkflowStep[];
63
+
64
+ export async function runWorkflow(workflow: Workflow, env: RuntimeEnvironment) {
65
+ for (const step of workflow) {
66
+ const def = TaskDefinitions.getInstance().getTask(step.taskName);
67
+ if (!def) throw new Error(`Task not found: ${step.taskName}`);
68
+ // eslint-disable-next-line no-await-in-loop
69
+ await def.action(step.args || {}, env);
70
+ }
71
+ }
72
+
73
+ export { task, subtask, extendEnvironment };
@@ -0,0 +1,8 @@
1
+ import dotenv from 'dotenv';
2
+ dotenv.config();
3
+
4
+ export const env = {
5
+ privateKey: process.env.STACKS_PRIVATE_KEY,
6
+ mainnetUrl: process.env.STACKS_MAINNET_URL,
7
+ testnetUrl: process.env.STACKS_TESTNET_URL,
8
+ };
@@ -0,0 +1,29 @@
1
+ import { RuntimeEnvironment } from './runtime-environment';
2
+
3
+ export type ExtensionFunc = (env: RuntimeEnvironment) => void;
4
+
5
+ export class Extender {
6
+ private static _instance: Extender;
7
+ private _extensions: ExtensionFunc[] = [];
8
+
9
+ private constructor() {}
10
+
11
+ public static getInstance(): Extender {
12
+ if (!Extender._instance) {
13
+ Extender._instance = new Extender();
14
+ }
15
+ return Extender._instance;
16
+ }
17
+
18
+ public addExtension(func: ExtensionFunc) {
19
+ this._extensions.push(func);
20
+ }
21
+
22
+ public getExtensions(): ExtensionFunc[] {
23
+ return this._extensions;
24
+ }
25
+ }
26
+
27
+ export function extendEnvironment(func: ExtensionFunc) {
28
+ Extender.getInstance().addExtension(func);
29
+ }
@@ -0,0 +1,74 @@
1
+ import { MobilestacksConfig, NetworkConfig } from '../types/config';
2
+ import { getAddressFromPrivateKey } from '@stacks/transactions';
3
+ import { generateWallet } from '@stacks/wallet-sdk';
4
+ import { StacksNetwork, createNetwork } from '@stacks/network';
5
+ import { Extender } from './extender';
6
+
7
+ export class RuntimeEnvironment {
8
+ public config: MobilestacksConfig;
9
+ public network: StacksNetwork;
10
+ public wallet!: { privateKey: string; address: string; accountIndex?: number };
11
+ public stacks: Record<string, unknown>; // Extension point
12
+ [key: string]: unknown; // Allow extensions
13
+
14
+ constructor(config: MobilestacksConfig) {
15
+ this.config = config;
16
+ const networkName = config.defaultNetwork;
17
+ const networkConfig: NetworkConfig = config.networks[networkName];
18
+ if (!networkConfig) {
19
+ throw new Error(`Network config not found for: ${networkName}`);
20
+ }
21
+
22
+ // Instantiate proper StacksNetwork
23
+ if (networkName === 'mainnet' || networkName === 'testnet') {
24
+ this.network = createNetwork({
25
+ network: networkName,
26
+ client: { baseUrl: networkConfig.url }
27
+ });
28
+ } else {
29
+ // Default to testnet structure for custom networks
30
+ this.network = createNetwork({
31
+ network: 'testnet',
32
+ client: { baseUrl: networkConfig.url }
33
+ });
34
+ }
35
+
36
+ // Wallet: support privateKey, seedPhrase+derivationPath, or both (prefer privateKey)
37
+ if (config.wallet.privateKey) {
38
+ // Derive address from privateKey
39
+ const address = getAddressFromPrivateKey(config.wallet.privateKey, networkName === 'mainnet' ? 'mainnet' : 'testnet');
40
+ this.wallet = {
41
+ privateKey: config.wallet.privateKey,
42
+ address,
43
+ };
44
+ } else if (config.wallet.seedPhrase) {
45
+ const path = config.wallet.derivationPath || "m/44'/5757'/0'/0/0"; // Stacks main path
46
+ // generateWallet requires a password param
47
+ generateWallet({ secretKey: config.wallet.seedPhrase, password: '' }).then(wallet => {
48
+ const index = parseInt(path.split('/').pop() || '0', 10);
49
+ const account = wallet.accounts[index] || wallet.accounts[0];
50
+ // Use getStxAddress to get the address
51
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
52
+ const { getStxAddress } = require('@stacks/wallet-sdk/dist/models/account');
53
+ // network.addressVersion has singleSig/multiSig lines.
54
+ const address = getStxAddress(account, this.network.transactionVersion);
55
+
56
+ this.wallet = {
57
+ privateKey: account.stxPrivateKey,
58
+ address,
59
+ accountIndex: account.index,
60
+ };
61
+ }).catch(() => {
62
+ throw new Error('Failed to generate wallet from seed phrase');
63
+ });
64
+ } else {
65
+ throw new Error('No privateKey or seedPhrase provided in wallet config');
66
+ }
67
+ this.stacks = {};
68
+
69
+ // Apply extensions
70
+ for (const extension of Extender.getInstance().getExtensions()) {
71
+ extension(this);
72
+ }
73
+ }
74
+ }