diff --git a/lib/actions/device.coffee b/lib/actions/device.coffee
deleted file mode 100644
index 4c51e99f..00000000
--- a/lib/actions/device.coffee
+++ /dev/null
@@ -1,443 +0,0 @@
-###
-Copyright 2016-2017 Balena
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-   http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-###
-
-commandOptions = require('./command-options')
-_ = require('lodash')
-{ normalizeUuidProp } = require('../utils/normalization')
-{ getBalenaSdk, getVisuals } = require('../utils/lazy')
-
-expandForAppName = {
-	$expand: belongs_to__application: $select: 'app_name'
-}
-
-exports.list =
-	signature: 'devices'
-	description: 'list all devices'
-	help: '''
-		Use this command to list all devices that belong to you.
-
-		You can filter the devices by application by using the `--application` option.
-
-		Examples:
-
-			$ balena devices
-			$ balena devices --application MyApp
-			$ balena devices --app MyApp
-			$ balena devices -a MyApp
-	'''
-	options: [ commandOptions.optionalApplication ]
-	permission: 'user'
-	primary: true
-	action: (params, options) ->
-		Promise = require('bluebird')
-		balena = getBalenaSdk()
-
-		Promise.try ->
-			if options.application?
-				return balena.models.device.getAllByApplication(options.application, expandForAppName)
-			return balena.models.device.getAll(expandForAppName)
-
-		.tap (devices) ->
-			devices = _.map devices, (device) ->
-				device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
-				device.application_name =
-					if device.belongs_to__application?[0] then device.belongs_to__application[0].app_name else 'N/a'
-				device.uuid = device.uuid.slice(0, 7)
-				return device
-
-			console.log getVisuals().table.horizontal devices, [
-				'id'
-				'uuid'
-				'device_name'
-				'device_type'
-				'application_name'
-				'status'
-				'is_online'
-				'supervisor_version'
-				'os_version'
-				'dashboard_url'
-			]
-
-exports.info =
-	signature: 'device <uuid>'
-	description: 'list a single device'
-	help: '''
-		Use this command to show information about a single device.
-
-		Examples:
-
-			$ balena device 7cf02a6
-	'''
-	permission: 'user'
-	primary: true
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-
-		balena.models.device.get(params.uuid, expandForAppName)
-		.then (device) ->
-			balena.models.device.getStatus(device).then (status) ->
-				device.status = status
-				device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
-				device.application_name =
-					if device.belongs_to__application?[0] then device.belongs_to__application[0].app_name else 'N/a'
-				device.commit = device.is_on__commit
-
-				console.log getVisuals().table.vertical device, [
-					"$#{device.device_name}$"
-					'id'
-					'device_type'
-					'status'
-					'is_online'
-					'ip_address'
-					'application_name'
-					'last_seen'
-					'uuid'
-					'commit'
-					'supervisor_version'
-					'is_web_accessible'
-					'note'
-					'os_version'
-					'dashboard_url'
-				]
-
-exports.register =
-	signature: 'device register <application>'
-	description: 'register a device'
-	help: '''
-		Use this command to register a device to an application.
-
-		Examples:
-
-			$ balena device register MyApp
-			$ balena device register MyApp --uuid <uuid>
-	'''
-	permission: 'user'
-	options: [
-		{
-			signature: 'uuid'
-			description: 'custom uuid'
-			parameter: 'uuid'
-			alias: 'u'
-		}
-	]
-	action: (params, options) ->
-		Promise = require('bluebird')
-		balena = getBalenaSdk()
-
-		Promise.join(
-			balena.models.application.get(params.application)
-			options.uuid ? balena.models.device.generateUniqueKey()
-			(application, uuid) ->
-				console.info("Registering to #{application.app_name}: #{uuid}")
-				return balena.models.device.register(application.id, uuid)
-		)
-		.get('uuid')
-
-exports.remove =
-	signature: 'device rm <uuid>'
-	description: 'remove a device'
-	help: '''
-		Use this command to remove a device from balena.
-
-		Notice this command asks for confirmation interactively.
-		You can avoid this by passing the `--yes` boolean option.
-
-		Examples:
-
-			$ balena device rm 7cf02a6
-			$ balena device rm 7cf02a6 --yes
-	'''
-	options: [ commandOptions.yes ]
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		patterns = require('../utils/patterns')
-
-		patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then ->
-			balena.models.device.remove(params.uuid)
-
-exports.identify =
-	signature: 'device identify <uuid>'
-	description: 'identify a device with a UUID'
-	help: '''
-		Use this command to identify a device.
-
-		In the Raspberry Pi, the ACT led is blinked several times.
-
-		Examples:
-
-			$ balena device identify 23c73a1
-	'''
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		balena.models.device.identify(params.uuid)
-
-exports.reboot =
-	signature: 'device reboot <uuid>'
-	description: 'restart a device'
-	help: '''
-		Use this command to remotely reboot a device
-
-		Examples:
-
-			$ balena device reboot 23c73a1
-	'''
-	options: [ commandOptions.forceUpdateLock ]
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		balena.models.device.reboot(params.uuid, options)
-
-exports.shutdown =
-	signature: 'device shutdown <uuid>'
-	description: 'shutdown a device'
-	help: '''
-		Use this command to remotely shutdown a device
-
-		Examples:
-
-			$ balena device shutdown 23c73a1
-	'''
-	options: [ commandOptions.forceUpdateLock ]
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		balena.models.device.shutdown(params.uuid, options)
-
-exports.enableDeviceUrl =
-	signature: 'device public-url enable <uuid>'
-	description: 'enable public URL for a device'
-	help: '''
-		Use this command to enable public URL for a device
-
-		Examples:
-
-			$ balena device public-url enable 23c73a1
-	'''
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		balena.models.device.enableDeviceUrl(params.uuid)
-
-exports.disableDeviceUrl =
-	signature: 'device public-url disable <uuid>'
-	description: 'disable public URL for a device'
-	help: '''
-		Use this command to disable public URL for a device
-
-		Examples:
-
-			$ balena device public-url disable 23c73a1
-	'''
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		balena.models.device.disableDeviceUrl(params.uuid)
-
-exports.getDeviceUrl =
-	signature: 'device public-url <uuid>'
-	description: 'gets the public URL of a device'
-	help: '''
-		Use this command to get the public URL of a device
-
-		Examples:
-
-			$ balena device public-url 23c73a1
-	'''
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		balena.models.device.getDeviceUrl(params.uuid).then (url) ->
-			console.log(url)
-
-exports.hasDeviceUrl =
-	signature: 'device public-url status <uuid>'
-	description: 'Returns true if public URL is enabled for a device'
-	help: '''
-		Use this command to determine if public URL is enabled for a device
-
-		Examples:
-
-			$ balena device public-url status 23c73a1
-	'''
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		balena.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
-			console.log(hasDeviceUrl)
-
-exports.rename =
-	signature: 'device rename <uuid> [newName]'
-	description: 'rename a balena device'
-	help: '''
-		Use this command to rename a device.
-
-		If you omit the name, you'll get asked for it interactively.
-
-		Examples:
-
-			$ balena device rename 7cf02a6
-			$ balena device rename 7cf02a6 MyPi
-	'''
-	permission: 'user'
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		Promise = require('bluebird')
-		balena = getBalenaSdk()
-		form = require('resin-cli-form')
-
-		Promise.try ->
-			return params.newName if not _.isEmpty(params.newName)
-
-			form.ask
-				message: 'How do you want to name this device?'
-				type: 'input'
-
-		.then(_.partial(balena.models.device.rename, params.uuid))
-
-exports.move =
-	signature: 'device move <uuid>'
-	description: 'move a device to another application'
-	help: '''
-		Use this command to move a device to another application you own.
-
-		If you omit the application, you'll get asked for it interactively.
-
-		Examples:
-
-			$ balena device move 7cf02a6
-			$ balena device move 7cf02a6 --application MyNewApp
-	'''
-	permission: 'user'
-	options: [ commandOptions.optionalApplication ]
-	action: (params, options) ->
-		normalizeUuidProp(params)
-		balena = getBalenaSdk()
-		patterns = require('../utils/patterns')
-
-		balena.models.device.get(params.uuid, expandForAppName).then (device) ->
-			device.application_name =
-				if device.belongs_to__application?[0] then device.belongs_to__application[0].app_name else 'N/a'
-			return options.application if options.application
-
-			return Promise.all([
-				balena.models.device.getManifestBySlug(device.device_type)
-				balena.models.config.getDeviceTypes()
-			]).then ([deviceDeviceType, deviceTypes]) ->
-				compatibleDeviceTypes = deviceTypes.filter (dt) ->
-					balena.models.os.isArchitectureCompatibleWith(deviceDeviceType.arch, dt.arch) &&
-					!!dt.isDependent == !!deviceDeviceType.isDependent &&
-					dt.state != 'DISCONTINUED'
-
-				return patterns.selectApplication (application) ->
-					return _.every [
-						_.some(compatibleDeviceTypes, (dt) -> dt.slug == application.device_type)
-						device.application_name isnt application.app_name
-					]
-		.tap (application) ->
-			return balena.models.device.move(params.uuid, application)
-		.then (application) ->
-			console.info("#{params.uuid} was moved to #{application}")
-
-exports.init =
-	signature: 'device init'
-	description: 'initialise a device with balenaOS'
-	help: '''
-		Use this command to download the OS image of a certain application and write it to an SD Card.
-
-		Notice this command may ask for confirmation interactively.
-		You can avoid this by passing the `--yes` boolean option.
-
-		Examples:
-
-			$ balena device init
-			$ balena device init --application MyApp
-	'''
-	options: [
-		commandOptions.optionalApplication
-		commandOptions.yes
-		commandOptions.advancedConfig
-		_.assign({}, commandOptions.osVersionOrSemver, { signature: 'os-version', parameter: 'os-version' })
-		commandOptions.drive
-		{
-			signature: 'config'
-			description: 'path to the config JSON file, see `balena os build-config`'
-			parameter: 'config'
-		}
-	]
-	permission: 'user'
-	action: (params, options) ->
-		Promise = require('bluebird')
-		rimraf = Promise.promisify(require('rimraf'))
-		tmp = require('tmp')
-		tmpNameAsync = Promise.promisify(tmp.tmpName)
-		tmp.setGracefulCleanup()
-
-		balena = getBalenaSdk()
-		patterns = require('../utils/patterns')
-		{ runCommand } = require('../utils/helpers')
-
-		Promise.try ->
-			return options.application if options.application?
-			return patterns.selectApplication()
-		.then(balena.models.application.get)
-		.then (application) ->
-
-			download = ->
-				tmpNameAsync().then (tempPath) ->
-					osVersion = options['os-version'] or 'default'
-					runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
-				.disposer (tempPath) ->
-					return rimraf(tempPath)
-
-			Promise.using download(), (tempPath) ->
-				runCommand("device register #{application.app_name}")
-					.then(balena.models.device.get)
-					.tap (device) ->
-						configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
-						if options.config
-							configureCommand += " --config '#{options.config}'"
-						else if options.advanced
-							configureCommand += ' --advanced'
-						runCommand(configureCommand)
-						.then ->
-							osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
-							if options.yes
-								osInitCommand += ' --yes'
-							if options.drive
-								osInitCommand += " --drive #{options.drive}"
-							runCommand(osInitCommand)
-						# Make sure the device resource is removed if there is an
-						# error when configuring or initializing a device image
-						.catch (error) ->
-							balena.models.device.remove(device.uuid).finally ->
-								throw error
-			.then (device) ->
-				console.log('Done')
-				return device.uuid
-
-tsActions = require('./device_ts')
-exports.osUpdate = tsActions.osUpdate
diff --git a/lib/actions/device.js b/lib/actions/device.js
new file mode 100644
index 00000000..8b5501c9
--- /dev/null
+++ b/lib/actions/device.js
@@ -0,0 +1,535 @@
+/*
+Copyright 2016-2020 Balena Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import * as commandOptions from './command-options';
+
+import * as _ from 'lodash';
+import { normalizeUuidProp } from '../utils/normalization';
+import { getBalenaSdk, getVisuals } from '../utils/lazy';
+
+/** @type {import('balena-sdk').PineOptionsFor<import('balena-sdk').Device>} */
+const expandForAppName = {
+	$expand: { belongs_to__application: { $select: 'app_name' } },
+};
+
+export const list = {
+	signature: 'devices',
+	description: 'list all devices',
+	help: `\
+Use this command to list all devices that belong to you.
+
+You can filter the devices by application by using the \`--application\` option.
+
+Examples:
+
+	$ balena devices
+	$ balena devices --application MyApp
+	$ balena devices --app MyApp
+	$ balena devices -a MyApp\
+`,
+	options: [commandOptions.optionalApplication],
+	permission: 'user',
+	primary: true,
+	action(_params, options) {
+		const Promise = require('bluebird');
+		const balena = getBalenaSdk();
+
+		return Promise.try(function() {
+			if (options.application != null) {
+				return balena.models.device.getAllByApplication(
+					options.application,
+					expandForAppName,
+				);
+			}
+			return balena.models.device.getAll(expandForAppName);
+		}).tap(function(devices) {
+			devices = _.map(devices, function(device) {
+				// @ts-ignore extending the device object with extra props
+				device.dashboard_url = balena.models.device.getDashboardUrl(
+					device.uuid,
+				);
+				// @ts-ignore extending the device object with extra props
+				device.application_name = device.belongs_to__application?.[0]
+					? device.belongs_to__application[0].app_name
+					: 'N/a';
+				device.uuid = device.uuid.slice(0, 7);
+				return device;
+			});
+
+			console.log(
+				getVisuals().table.horizontal(devices, [
+					'id',
+					'uuid',
+					'device_name',
+					'device_type',
+					'application_name',
+					'status',
+					'is_online',
+					'supervisor_version',
+					'os_version',
+					'dashboard_url',
+				]),
+			);
+		});
+	},
+};
+
+export const info = {
+	signature: 'device <uuid>',
+	description: 'list a single device',
+	help: `\
+Use this command to show information about a single device.
+
+Examples:
+
+	$ balena device 7cf02a6\
+`,
+	permission: 'user',
+	primary: true,
+	action(params) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+
+		return balena.models.device
+			.get(params.uuid, expandForAppName)
+			.then(device =>
+				// @ts-ignore `device.getStatus` requires a device with service info, but
+				// this device isn't typed with them, possibly needs fixing?
+				balena.models.device.getStatus(device).then(function(status) {
+					device.status = status;
+					// @ts-ignore extending the device object with extra props
+					device.dashboard_url = balena.models.device.getDashboardUrl(
+						device.uuid,
+					);
+					// @ts-ignore extending the device object with extra props
+					device.application_name = device.belongs_to__application?.[0]
+						? device.belongs_to__application[0].app_name
+						: 'N/a';
+					// @ts-ignore extending the device object with extra props
+					device.commit = device.is_on__commit;
+
+					console.log(
+						getVisuals().table.vertical(device, [
+							`$${device.device_name}$`,
+							'id',
+							'device_type',
+							'status',
+							'is_online',
+							'ip_address',
+							'application_name',
+							'last_seen',
+							'uuid',
+							'commit',
+							'supervisor_version',
+							'is_web_accessible',
+							'note',
+							'os_version',
+							'dashboard_url',
+						]),
+					);
+				}),
+			);
+	},
+};
+
+export const register = {
+	signature: 'device register <application>',
+	description: 'register a device',
+	help: `\
+Use this command to register a device to an application.
+
+Examples:
+
+	$ balena device register MyApp
+	$ balena device register MyApp --uuid <uuid>\
+`,
+	permission: 'user',
+	options: [
+		{
+			signature: 'uuid',
+			description: 'custom uuid',
+			parameter: 'uuid',
+			alias: 'u',
+		},
+	],
+	action(params, options) {
+		const Promise = require('bluebird');
+		const balena = getBalenaSdk();
+
+		return Promise.join(
+			balena.models.application.get(params.application),
+			options.uuid ?? balena.models.device.generateUniqueKey(),
+			function(application, uuid) {
+				console.info(`Registering to ${application.app_name}: ${uuid}`);
+				return balena.models.device.register(application.id, uuid);
+			},
+		).get('uuid');
+	},
+};
+
+export const remove = {
+	signature: 'device rm <uuid>',
+	description: 'remove a device',
+	help: `\
+Use this command to remove a device from balena.
+
+Notice this command asks for confirmation interactively.
+You can avoid this by passing the \`--yes\` boolean option.
+
+Examples:
+
+	$ balena device rm 7cf02a6
+	$ balena device rm 7cf02a6 --yes\
+`,
+	options: [commandOptions.yes],
+	permission: 'user',
+	action(params, options) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		const patterns = require('../utils/patterns');
+
+		return patterns
+			.confirm(options.yes, 'Are you sure you want to delete the device?')
+			.then(() => balena.models.device.remove(params.uuid));
+	},
+};
+
+export const identify = {
+	signature: 'device identify <uuid>',
+	description: 'identify a device with a UUID',
+	help: `\
+Use this command to identify a device.
+
+In the Raspberry Pi, the ACT led is blinked several times.
+
+Examples:
+
+	$ balena device identify 23c73a1\
+`,
+	permission: 'user',
+	action(params) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		return balena.models.device.identify(params.uuid);
+	},
+};
+
+export const reboot = {
+	signature: 'device reboot <uuid>',
+	description: 'restart a device',
+	help: `\
+Use this command to remotely reboot a device
+
+Examples:
+
+	$ balena device reboot 23c73a1\
+`,
+	options: [commandOptions.forceUpdateLock],
+	permission: 'user',
+	action(params, options) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		return balena.models.device.reboot(params.uuid, options);
+	},
+};
+
+export const shutdown = {
+	signature: 'device shutdown <uuid>',
+	description: 'shutdown a device',
+	help: `\
+Use this command to remotely shutdown a device
+
+Examples:
+
+	$ balena device shutdown 23c73a1\
+`,
+	options: [commandOptions.forceUpdateLock],
+	permission: 'user',
+	action(params, options) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		return balena.models.device.shutdown(params.uuid, options);
+	},
+};
+
+export const enableDeviceUrl = {
+	signature: 'device public-url enable <uuid>',
+	description: 'enable public URL for a device',
+	help: `\
+Use this command to enable public URL for a device
+
+Examples:
+
+	$ balena device public-url enable 23c73a1\
+`,
+	permission: 'user',
+	action(params) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		return balena.models.device.enableDeviceUrl(params.uuid);
+	},
+};
+
+export const disableDeviceUrl = {
+	signature: 'device public-url disable <uuid>',
+	description: 'disable public URL for a device',
+	help: `\
+Use this command to disable public URL for a device
+
+Examples:
+
+	$ balena device public-url disable 23c73a1\
+`,
+	permission: 'user',
+	action(params) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		return balena.models.device.disableDeviceUrl(params.uuid);
+	},
+};
+
+export const getDeviceUrl = {
+	signature: 'device public-url <uuid>',
+	description: 'gets the public URL of a device',
+	help: `\
+Use this command to get the public URL of a device
+
+Examples:
+
+	$ balena device public-url 23c73a1\
+`,
+	permission: 'user',
+	action(params) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		return balena.models.device.getDeviceUrl(params.uuid).then(url => {
+			console.log(url);
+		});
+	},
+};
+
+export const hasDeviceUrl = {
+	signature: 'device public-url status <uuid>',
+	description: 'Returns true if public URL is enabled for a device',
+	help: `\
+Use this command to determine if public URL is enabled for a device
+
+Examples:
+
+	$ balena device public-url status 23c73a1\
+`,
+	permission: 'user',
+	action(params) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		return balena.models.device.hasDeviceUrl(params.uuid).then(hasUrl => {
+			console.log(hasUrl);
+		});
+	},
+};
+
+export const rename = {
+	signature: 'device rename <uuid> [newName]',
+	description: 'rename a balena device',
+	help: `\
+Use this command to rename a device.
+
+If you omit the name, you'll get asked for it interactively.
+
+Examples:
+
+	$ balena device rename 7cf02a6
+	$ balena device rename 7cf02a6 MyPi\
+`,
+	permission: 'user',
+	action(params) {
+		normalizeUuidProp(params);
+		const Promise = require('bluebird');
+		const balena = getBalenaSdk();
+		const form = require('resin-cli-form');
+
+		return Promise.try(function() {
+			if (!_.isEmpty(params.newName)) {
+				return params.newName;
+			}
+
+			return form.ask({
+				message: 'How do you want to name this device?',
+				type: 'input',
+			});
+		}).then(_.partial(balena.models.device.rename, params.uuid));
+	},
+};
+
+export const move = {
+	signature: 'device move <uuid>',
+	description: 'move a device to another application',
+	help: `\
+Use this command to move a device to another application you own.
+
+If you omit the application, you'll get asked for it interactively.
+
+Examples:
+
+	$ balena device move 7cf02a6
+	$ balena device move 7cf02a6 --application MyNewApp\
+`,
+	permission: 'user',
+	options: [commandOptions.optionalApplication],
+	action(params, options) {
+		normalizeUuidProp(params);
+		const balena = getBalenaSdk();
+		const patterns = require('../utils/patterns');
+
+		return balena.models.device
+			.get(params.uuid, expandForAppName)
+			.then(function(device) {
+				// @ts-ignore extending the device object with extra props
+				device.application_name = device.belongs_to__application?.[0]
+					? device.belongs_to__application[0].app_name
+					: 'N/a';
+				if (options.application) {
+					return options.application;
+				}
+
+				return Promise.all([
+					balena.models.device.getManifestBySlug(device.device_type),
+					balena.models.config.getDeviceTypes(),
+				]).then(function([deviceDeviceType, deviceTypes]) {
+					const compatibleDeviceTypes = deviceTypes.filter(
+						dt =>
+							balena.models.os.isArchitectureCompatibleWith(
+								deviceDeviceType.arch,
+								dt.arch,
+							) &&
+							!!dt.isDependent === !!deviceDeviceType.isDependent &&
+							dt.state !== 'DISCONTINUED',
+					);
+
+					return patterns.selectApplication(application =>
+						_.every([
+							_.some(
+								compatibleDeviceTypes,
+								dt => dt.slug === application.device_type,
+							),
+							// @ts-ignore using the extended device object prop
+							device.application_name !== application.app_name,
+						]),
+					);
+				});
+			})
+			.tap(application => balena.models.device.move(params.uuid, application))
+			.then(application => {
+				console.info(`${params.uuid} was moved to ${application}`);
+			});
+	},
+};
+
+export const init = {
+	signature: 'device init',
+	description: 'initialise a device with balenaOS',
+	help: `\
+Use this command to download the OS image of a certain application and write it to an SD Card.
+
+Notice this command may ask for confirmation interactively.
+You can avoid this by passing the \`--yes\` boolean option.
+
+Examples:
+
+	$ balena device init
+	$ balena device init --application MyApp\
+`,
+	options: [
+		commandOptions.optionalApplication,
+		commandOptions.yes,
+		commandOptions.advancedConfig,
+		_.assign({}, commandOptions.osVersionOrSemver, {
+			signature: 'os-version',
+			parameter: 'os-version',
+		}),
+		commandOptions.drive,
+		{
+			signature: 'config',
+			description: 'path to the config JSON file, see `balena os build-config`',
+			parameter: 'config',
+		},
+	],
+	permission: 'user',
+	action(_params, options) {
+		const Promise = require('bluebird');
+		const rimraf = Promise.promisify(require('rimraf'));
+		const tmp = require('tmp');
+		const tmpNameAsync = Promise.promisify(tmp.tmpName);
+		tmp.setGracefulCleanup();
+
+		const balena = getBalenaSdk();
+		const patterns = require('../utils/patterns');
+		const { runCommand } = require('../utils/helpers');
+
+		return Promise.try(function() {
+			if (options.application != null) {
+				return options.application;
+			}
+			return patterns.selectApplication();
+		})
+			.then(balena.models.application.get)
+			.then(function(application) {
+				const download = () =>
+					tmpNameAsync()
+						.then(function(tempPath) {
+							const osVersion = options['os-version'] || 'default';
+							return runCommand(
+								`os download ${application.device_type} --output '${tempPath}' --version ${osVersion}`,
+							);
+						})
+						.disposer(tempPath => rimraf(tempPath));
+
+				return Promise.using(download(), tempPath =>
+					runCommand(`device register ${application.app_name}`)
+						.then(balena.models.device.get)
+						.tap(function(device) {
+							let configureCommand = `os configure '${tempPath}' --device ${device.uuid}`;
+							if (options.config) {
+								configureCommand += ` --config '${options.config}'`;
+							} else if (options.advanced) {
+								configureCommand += ' --advanced';
+							}
+							return runCommand(configureCommand)
+								.then(function() {
+									let osInitCommand = `os initialize '${tempPath}' --type ${application.device_type}`;
+									if (options.yes) {
+										osInitCommand += ' --yes';
+									}
+									if (options.drive) {
+										osInitCommand += ` --drive ${options.drive}`;
+									}
+									return runCommand(osInitCommand);
+								})
+								.catch(error =>
+									balena.models.device.remove(device.uuid).finally(function() {
+										throw error;
+									}),
+								);
+						}),
+				).then(function(device) {
+					console.log('Done');
+					return device.uuid;
+				});
+			});
+	},
+};
+
+export { osUpdate } from './device_ts';
diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts
index 207f5696..5a092954 100644
--- a/lib/utils/helpers.ts
+++ b/lib/utils/helpers.ts
@@ -94,7 +94,7 @@ export async function sudo(
 	await executeWithPrivileges(command, stderr, isCLIcmd);
 }
 
-export function runCommand(command: string): Bluebird<void> {
+export function runCommand<T>(command: string): Bluebird<T> {
 	const capitano = require('capitano');
 	return Bluebird.fromCallback(resolver => capitano.run(command, resolver));
 }
diff --git a/lib/utils/patterns.ts b/lib/utils/patterns.ts
index 6797ffd3..5b8706c4 100644
--- a/lib/utils/patterns.ts
+++ b/lib/utils/patterns.ts
@@ -179,7 +179,7 @@ export function confirm(
 }
 
 export function selectApplication(
-	filter: (app: BalenaSdk.Application) => boolean,
+	filter?: (app: BalenaSdk.Application) => boolean,
 ) {
 	const balena = getBalenaSdk();
 	return balena.models.application
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index d69074ff..ed8d53d6 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1336,6 +1336,12 @@
         "@types/node": "*"
       }
     },
+    "@types/tmp": {
+      "version": "0.0.34",
+      "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.34.tgz",
+      "integrity": "sha512-Tx7JYeYR+pkAnDQjN1Cj43KuOuUvyybZHl+fAezReXuH/SQoxLhsuPvHZH/SA4XtrBEhaTcbb5gVc1WQcjQgdg==",
+      "dev": true
+    },
     "@types/tough-cookie": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz",
diff --git a/package.json b/package.json
index b7df6412..cc6fc2ff 100644
--- a/package.json
+++ b/package.json
@@ -138,6 +138,7 @@
     "@types/stream-to-promise": "2.2.0",
     "@types/tar-stream": "^2.1.0",
     "@types/through2": "^2.0.34",
+    "@types/tmp": "0.0.34",
     "@types/which": "^1.3.2",
     "catch-uncommitted": "^1.5.0",
     "chai": "^4.2.0",