diff --git a/package-lock.json b/package-lock.json index cdfdccf..6644e1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@clack/prompts": "1.2.0", "@cloudcannon/gadget": "0.0.32", + "@cloudcannon/sdk": "0.0.2", "citty": "0.2.2", "yaml": "2.8.3" }, @@ -251,6 +252,15 @@ "node": ">=24" } }, + "node_modules/@cloudcannon/sdk": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@cloudcannon/sdk/-/sdk-0.0.2.tgz", + "integrity": "sha512-DOkN1SmU1gy40lf0oL0kPw099BgfDwDYjVQwnGA1rtoFODTzVPNcjVqc1W0Y5ygHG8fUSHY99yRalT4W+6uCfQ==", + "license": "ISC", + "engines": { + "node": ">=20" + } + }, "node_modules/@sindresorhus/slugify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-3.0.0.tgz", diff --git a/package.json b/package.json index e9778f5..b32c600 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "dependencies": { "@clack/prompts": "1.2.0", "@cloudcannon/gadget": "0.0.32", + "@cloudcannon/sdk": "0.0.2", "citty": "0.2.2", "yaml": "2.8.3" } diff --git a/src/builds.ts b/src/builds.ts new file mode 100644 index 0000000..f80b40f --- /dev/null +++ b/src/builds.ts @@ -0,0 +1,56 @@ +import { defineCommand } from 'citty'; +import { printJson } from './configure/utility.ts'; +import { getSdkClient } from './sdk-client.ts'; + +export const buildsListCommand = defineCommand({ + meta: { + name: 'list', + description: 'List builds for a site.', + }, + args: { + site: { + type: 'string', + description: 'The site UUID', + valueHint: 'uuid', + required: true, + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + const builds = await client.site(ctx.args.site).getBuilds(); + printJson(builds); + }, +}); + +export const buildsPrintLogsCommand = defineCommand({ + meta: { + name: 'print-logs', + description: 'Prints the logs for a build.', + }, + args: { + uuid: { + type: 'positional', + description: 'The build UUID', + required: true, + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + const resp = await client.build(ctx.args.uuid).get(); + const text = await resp.text(); + if (text) { + console.log(text); + } + }, +}); + +export const buildsCommand = defineCommand({ + meta: { + name: 'builds', + description: 'Manage CloudCannon builds.', + }, + subCommands: { + list: buildsListCommand, + 'print-logs': buildsPrintLogsCommand, + }, +}); diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..6a44650 --- /dev/null +++ b/src/files.ts @@ -0,0 +1,119 @@ +import { createWriteStream } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { Writable } from 'node:stream'; +import { defineCommand } from 'citty'; +import { printJson } from './configure/utility.ts'; +import { getSdkClient } from './sdk-client.ts'; + +export const filesListCommand = defineCommand({ + meta: { + name: 'list', + description: 'List files from a site.', + }, + args: { + site: { + type: 'string', + description: 'The site UUID', + valueHint: 'uuid', + required: true, + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + const files = await client.site(ctx.args.site).listFiles(); + const output = Object.fromEntries(files.map((file) => [file.sitePath, file.md5])); + printJson(output); + }, +}); + +export const filesGetCommand = defineCommand({ + meta: { + name: 'get', + description: 'Get the contents of a file from a site.', + }, + args: { + site: { + type: 'string', + description: 'The site UUID', + valueHint: 'uuid', + required: true, + }, + output: { + type: 'string', + description: 'Path to save the file to', + valueHint: 'path', + }, + path: { + type: 'positional', + description: 'The file path on the site', + required: true, + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + const resp = await client.site(ctx.args.site).getFile(ctx.args.path); + if (ctx.args.output) { + const stream = createWriteStream(ctx.args.output); + await resp.body?.pipeTo(Writable.toWeb(stream)); + } else { + const text = await resp.text(); + console.log(text); + } + }, +}); + +export const filesUploadCommand = defineCommand({ + meta: { + name: 'upload', + description: 'Upload a file to a site.', + }, + args: { + site: { + type: 'string', + description: 'The site UUID', + valueHint: 'uuid', + required: true, + }, + localPath: { + type: 'positional', + description: 'The local file path to upload', + required: true, + }, + path: { + type: 'positional', + description: 'The destination path on the site', + required: true, + }, + type: { + type: 'string', + description: 'MIME type of the file', + valueHint: 'mime', + }, + overwrite: { + type: 'boolean', + description: 'Overwrite if the file already exists', + default: false, + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + const content = await readFile(ctx.args.localPath); + await client.site(ctx.args.site).uploadFile(ctx.args.path, content, { + type: ctx.args.type, + overwriteExistingFile: ctx.args.overwrite, + }); + console.log('File uploaded.'); + }, +}); + +export const filesCommand = defineCommand({ + meta: { + name: 'files', + description: 'Manage files on CloudCannon sites.', + }, + subCommands: { + list: filesListCommand, + get: filesGetCommand, + upload: filesUploadCommand, + }, +}); diff --git a/src/index.ts b/src/index.ts index 6326fa9..9640de3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,10 @@ import { defineCommand, runMain } from 'citty'; import pkg from '../package.json' with { type: 'json' }; +import { buildsCommand } from './builds.ts'; import { configureCommand } from './configure.ts'; +import { orgsCommand } from './orgs.ts'; +import { sitesCommand } from './site.ts'; const main = defineCommand({ meta: { @@ -11,7 +14,10 @@ const main = defineCommand({ description: 'Work with CloudCannon from the command line.', }, subCommands: { + builds: buildsCommand, configure: configureCommand, + orgs: orgsCommand, + sites: sitesCommand, }, }); diff --git a/src/orgs.ts b/src/orgs.ts new file mode 100644 index 0000000..70233b6 --- /dev/null +++ b/src/orgs.ts @@ -0,0 +1,45 @@ +import { defineCommand } from 'citty'; +import { printJson } from './configure/utility.ts'; +import { getSdkClient } from './sdk-client.ts'; + +export const orgsListCommand = defineCommand({ + meta: { + name: 'list', + description: 'List all organisations.', + }, + async run(): Promise { + const client = getSdkClient(); + const orgs = await client.orgs(); + printJson(orgs); + }, +}); + +export const orgsGetCommand = defineCommand({ + meta: { + name: 'get', + description: 'Get an organisation by UUID.', + }, + args: { + uuid: { + type: 'positional', + description: 'The organisation UUID', + required: true, + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + const org = await client.org(ctx.args.uuid).get(); + printJson(org); + }, +}); + +export const orgsCommand = defineCommand({ + meta: { + name: 'orgs', + description: 'Manage CloudCannon organisations.', + }, + subCommands: { + list: orgsListCommand, + get: orgsGetCommand, + }, +}); diff --git a/src/sdk-client.ts b/src/sdk-client.ts new file mode 100644 index 0000000..45ffb28 --- /dev/null +++ b/src/sdk-client.ts @@ -0,0 +1,11 @@ +import CloudCannonClient from '@cloudcannon/sdk'; + +export function getSdkClient(): CloudCannonClient { + const apiKey = process.env.CLOUDCANNON_API_KEY; + if (!apiKey) { + throw new Error( + 'CLOUDCANNON_API_KEY environment variable is required. Set it with: export CLOUDCANNON_API_KEY=your_key' + ); + } + return new CloudCannonClient({ key: apiKey }); +} diff --git a/src/site.ts b/src/site.ts new file mode 100644 index 0000000..82eefde --- /dev/null +++ b/src/site.ts @@ -0,0 +1,231 @@ +import { defineCommand } from 'citty'; +import { printJson } from './configure/utility.ts'; +import { filesGetCommand, filesListCommand, filesUploadCommand } from './files.ts'; +import { getSdkClient } from './sdk-client.ts'; + +export const sitesListCommand = defineCommand({ + meta: { + name: 'list', + description: 'List all sites.', + }, + args: { + org: { + type: 'string', + description: 'Limit to a specific organisation UUID', + valueHint: 'uuid', + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + + if (ctx.args.org) { + const sites = await client.org(ctx.args.org).sites(); + printJson(sites); + return; + } + + const orgs = await client.orgs(); + const allSites = []; + for (const org of orgs) { + const sites = await client.org(org.uuid).sites(); + allSites.push(...sites); + } + printJson(allSites); + }, +}); + +export const sitesGetCommand = defineCommand({ + meta: { + name: 'get', + description: 'Get a site by UUID.', + }, + args: { + uuid: { + type: 'positional', + description: 'The site UUID', + required: true, + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + const site = await client.site(ctx.args.uuid).get(); + printJson(site); + }, +}); + +export const sitesRebuildCommand = defineCommand({ + meta: { + name: 'rebuild', + description: 'Trigger a rebuild for a site.', + }, + args: { + uuid: { + type: 'positional', + description: 'The site UUID', + required: true, + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + await client.site(ctx.args.uuid).rebuild(); + console.log('Rebuild triggered.'); + }, +}); + +export const sitesUpdateBuildConfigCommand = defineCommand({ + meta: { + name: 'update-build-config', + description: 'Update the build configuration for a site.', + }, + args: { + uuid: { + type: 'positional', + description: 'The site UUID', + required: true, + }, + ssg: { + type: 'string', + description: 'Static site generator name', + valueHint: 'name', + }, + 'building-locked': { + type: 'boolean', + description: 'Lock the site from building', + }, + 'uses-i18n': { + type: 'boolean', + description: 'Enable i18n support', + }, + 'default-locale': { + type: 'string', + description: 'Default locale for i18n', + valueHint: 'locale', + }, + 'install-command': { + type: 'string', + description: 'Override install command', + valueHint: 'cmd', + }, + 'build-command': { + type: 'string', + description: 'Override build command', + valueHint: 'cmd', + }, + 'output-path': { + type: 'string', + description: 'Override output path', + valueHint: 'path', + }, + 'preserved-paths': { + type: 'string', + description: 'Comma-separated preserved paths', + valueHint: 'paths', + }, + 'hugo-version': { + type: 'string', + description: 'Hugo version', + }, + 'node-version': { + type: 'string', + description: 'Node version', + }, + 'ruby-version': { + type: 'string', + description: 'Ruby version', + }, + 'deno-version': { + type: 'string', + description: 'Deno version', + }, + 'preserve-output': { + type: 'boolean', + description: 'Preserve previous output', + }, + 'include-git': { + type: 'boolean', + description: 'Include git history in build', + }, + }, + async run(ctx): Promise { + const client = getSdkClient(); + + const options: Record = {}; + if (ctx.args.ssg !== undefined) { + options.ssg = ctx.args.ssg; + } + if (ctx.args.buildingLocked !== undefined) { + options.building_locked = ctx.args.buildingLocked; + } + if (ctx.args.usesI18n !== undefined) { + options.uses_i18n = ctx.args.usesI18n; + } + if (ctx.args.defaultLocale !== undefined) { + options.default_locale = ctx.args.defaultLocale; + } + + const compile: Record = {}; + if (ctx.args.installCommand !== undefined) { + compile.install_command = ctx.args.installCommand; + } + if (ctx.args.buildCommand !== undefined) { + compile.build_command = ctx.args.buildCommand; + } + if (ctx.args.outputPath !== undefined) { + compile.output_path = ctx.args.outputPath; + } + if (ctx.args.preservedPaths !== undefined && typeof ctx.args.preservedPaths === 'string') { + compile.preserved_paths = ctx.args.preservedPaths.split(','); + } + if (ctx.args.hugoVersion !== undefined) { + compile.hugoVersion = ctx.args.hugoVersion; + } + if (ctx.args.nodeVersion !== undefined) { + compile.nodeVersion = ctx.args.nodeVersion; + } + if (ctx.args.rubyVersion !== undefined) { + compile.rubyVersion = ctx.args.rubyVersion; + } + if (ctx.args.denoVersion !== undefined) { + compile.denoVersion = ctx.args.denoVersion; + } + if (ctx.args.preserveOutput !== undefined) { + compile.preserveOutput = ctx.args.preserveOutput; + } + if (ctx.args.includeGit !== undefined) { + compile.includeGit = ctx.args.includeGit; + } + + if (Object.keys(compile).length > 0) { + options.compile = compile; + } + + const site = await client.site(ctx.args.uuid).updateBuildConfig(options); + printJson(site); + }, +}); + +export const sitesFilesCommand = defineCommand({ + meta: { + name: 'files', + description: 'Manage files on a CloudCannon site.', + }, + subCommands: { + list: filesListCommand, + get: filesGetCommand, + upload: filesUploadCommand, + }, +}); + +export const sitesCommand = defineCommand({ + meta: { + name: 'sites', + description: 'Manage CloudCannon sites.', + }, + subCommands: { + list: sitesListCommand, + get: sitesGetCommand, + rebuild: sitesRebuildCommand, + 'update-build-config': sitesUpdateBuildConfigCommand, + files: sitesFilesCommand, + }, +});