From 626bcb972f52e421becf8175c7e578dc89367ca9 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 14:26:10 +0200 Subject: [PATCH 01/16] feat: cmd changes list and apply --- src/commands/changes.ts | 35 ++++++++++++++++++++++++++++++++ src/constants/help.constants.ts | 4 ++++ src/help/changes.apply.help.ts | 29 ++++++++++++++++++++++++++ src/help/changes.help.ts | 36 +++++++++++++++++++++++++++++++++ src/help/changes.list.help.ts | 27 +++++++++++++++++++++++++ src/index.ts | 7 +++++++ 6 files changed, 138 insertions(+) create mode 100644 src/commands/changes.ts create mode 100644 src/help/changes.apply.help.ts create mode 100644 src/help/changes.help.ts create mode 100644 src/help/changes.list.help.ts diff --git a/src/commands/changes.ts b/src/commands/changes.ts new file mode 100644 index 00000000..d67c1e73 --- /dev/null +++ b/src/commands/changes.ts @@ -0,0 +1,35 @@ +import {red} from 'kleur'; +import {logHelpChangesApply} from '../help/changes.apply.help'; +import {logHelpChanges} from '../help/changes.help'; +import {logHelpChangesList} from '../help/changes.list.help'; + +export const changes = async (args?: string[]) => { + const [subCommand] = args ?? []; + + switch (subCommand) { + case 'list': + // TODO + break; + case 'apply': + // TODO + break; + default: + console.log(red('Unknown subcommand.')); + logHelpChanges(); + } +}; + +export const helpChanges = (args?: string[]) => { + const [subCommand] = args ?? []; + + switch (subCommand) { + case 'list': + logHelpChangesList(args); + break; + case 'apply': + logHelpChangesApply(args); + break; + default: + logHelpChanges(args); + } +}; diff --git a/src/constants/help.constants.ts b/src/constants/help.constants.ts index 688dfee7..5ae7d98d 100644 --- a/src/constants/help.constants.ts +++ b/src/constants/help.constants.ts @@ -1,5 +1,6 @@ import {magenta} from 'kleur'; +export const CHANGES_DESCRIPTION = 'Review and apply changes submitted to your module.'; export const CLEAR_DESCRIPTION = 'Clear existing app code by removing JavaScript, HTML, CSS, and other files from your satellite.'; export const CONFIG_DESCRIPTION = 'Apply configuration to satellite.'; @@ -30,3 +31,6 @@ export const DEV_BUILD_NOTES = `- If no language is provided, the CLI attempts t - Language can be shortened to ${magenta('rs')} for Rust, ${magenta('ts')} for TypeScript and ${magenta('mjs')} for JavaScript. - The path option maps to ${magenta('--manifest-path')} for Rust (Cargo) or to the source file for TypeScript and JavaScript (e.g. ${magenta('index.ts')} or ${magenta('index.mjs')}). - The watch option rebuilds when source files change, with a default debounce delay of 10 seconds; optionally, pass a delay in milliseconds.`; + +export const CHANGES_LIST_DESCRIPTION = 'List all submitted or applied changes.'; +export const CHANGES_APPLY_DESCRIPTION = 'Apply a submitted change.'; diff --git a/src/help/changes.apply.help.ts b/src/help/changes.apply.help.ts new file mode 100644 index 00000000..d79c0bde --- /dev/null +++ b/src/help/changes.apply.help.ts @@ -0,0 +1,29 @@ +import {cyan, green, magenta, yellow} from 'kleur'; +import {CHANGES_APPLY_DESCRIPTION} from '../constants/help.constants'; +import {helpOutput} from './common.help'; +import {TITLE} from './help'; + +const usage = `Usage: ${green('juno')} ${cyan('changes')} ${magenta('apply')} ${yellow('[options]')} + +Options: + ${yellow('-i, --id')} The ID of the change to apply. + ${yellow('-s, --hash')} The expected hash of all included changes (for verification). + ${yellow('-h, --help')} Output usage information.`; + +const doc = `${CHANGES_APPLY_DESCRIPTION} + +\`\`\` +${usage} +\`\`\` +`; + +const help = `${TITLE} + +${CHANGES_APPLY_DESCRIPTION} + +${usage} +`; + +export const logHelpChangesApply = (args?: string[]) => { + console.log(helpOutput(args) === 'doc' ? doc : help); +}; diff --git a/src/help/changes.help.ts b/src/help/changes.help.ts new file mode 100644 index 00000000..6216c3a4 --- /dev/null +++ b/src/help/changes.help.ts @@ -0,0 +1,36 @@ +import {cyan, green, magenta, yellow} from 'kleur'; +import { + CHANGES_APPLY_DESCRIPTION, + CHANGES_DESCRIPTION, + CHANGES_LIST_DESCRIPTION +} from '../constants/help.constants'; +import {helpOutput} from './common.help'; +import {TITLE} from './help'; + +const helpChangesList = `${magenta('list')} ${CHANGES_LIST_DESCRIPTION}`; + +const helpChangesApply = `${magenta('apply')} ${CHANGES_APPLY_DESCRIPTION}`; + +const usage = `Usage: ${green('juno')} ${cyan('changes')} ${magenta('')} ${yellow('[options]')} + +Subcommands: + ${helpChangesApply} + ${helpChangesList}`; + +const doc = `${CHANGES_DESCRIPTION} + +\`\`\` +${usage} +\`\`\` +`; + +const help = `${TITLE} + +${CHANGES_DESCRIPTION} + +${usage} +`; + +export const logHelpChanges = (args?: string[]) => { + console.log(helpOutput(args) === 'doc' ? doc : help); +}; diff --git a/src/help/changes.list.help.ts b/src/help/changes.list.help.ts new file mode 100644 index 00000000..4780254f --- /dev/null +++ b/src/help/changes.list.help.ts @@ -0,0 +1,27 @@ +import {cyan, green, magenta, yellow} from 'kleur'; +import {CHANGES_LIST_DESCRIPTION} from '../constants/help.constants'; +import {helpOutput} from './common.help'; +import {TITLE} from './help'; + +const usage = `Usage: ${green('juno')} ${cyan('changes')} ${magenta('list')} ${yellow('[options]')} + +Options: + ${yellow('-h, --help')} Output usage information.`; + +const doc = `${CHANGES_LIST_DESCRIPTION} + +\`\`\` +${usage} +\`\`\` +`; + +const help = `${TITLE} + +${CHANGES_LIST_DESCRIPTION} + +${usage} +`; + +export const logHelpChangesList = (args?: string[]) => { + console.log(helpOutput(args) === 'doc' ? doc : help); +}; diff --git a/src/index.ts b/src/index.ts index 9f0b684c..d472c317 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import {hasArgs} from '@junobuild/cli-tools'; import {red} from 'kleur'; import {login, logout} from './commands/auth'; +import {changes, helpChanges} from './commands/changes'; import {clear} from './commands/clear'; import {config} from './commands/config'; import {deploy} from './commands/deploy'; @@ -103,6 +104,9 @@ export const run = async () => { case 'start': logHelpStart(args); break; + case 'changes': + helpChanges(args); + break; default: console.log(red('Unknown command.')); console.log(help); @@ -156,6 +160,9 @@ export const run = async () => { case 'snapshot': await snapshot(args); break; + case 'changes': + await changes(args); + break; case 'help': console.log(help); break; From 05edcfb61ca5ab3019845d1fbdb57986a53c344b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 17:25:12 +0200 Subject: [PATCH 02/16] feat: list changes --- src/commands/changes.ts | 3 +- src/help/changes.list.help.ts | 1 + src/services/changes/changes.list.services.ts | 53 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/services/changes/changes.list.services.ts diff --git a/src/commands/changes.ts b/src/commands/changes.ts index d67c1e73..75409b42 100644 --- a/src/commands/changes.ts +++ b/src/commands/changes.ts @@ -2,13 +2,14 @@ import {red} from 'kleur'; import {logHelpChangesApply} from '../help/changes.apply.help'; import {logHelpChanges} from '../help/changes.help'; import {logHelpChangesList} from '../help/changes.list.help'; +import {listChanges} from '../services/changes/changes.list.services'; export const changes = async (args?: string[]) => { const [subCommand] = args ?? []; switch (subCommand) { case 'list': - // TODO + await listChanges(args); break; case 'apply': // TODO diff --git a/src/help/changes.list.help.ts b/src/help/changes.list.help.ts index 4780254f..5bd586c3 100644 --- a/src/help/changes.list.help.ts +++ b/src/help/changes.list.help.ts @@ -6,6 +6,7 @@ import {TITLE} from './help'; const usage = `Usage: ${green('juno')} ${cyan('changes')} ${magenta('list')} ${yellow('[options]')} Options: + ${yellow('-a, --all')} Search through all changes, not just the 100 most recent. ${yellow('-h, --help')} Output usage information.`; const doc = `${CHANGES_LIST_DESCRIPTION} diff --git a/src/services/changes/changes.list.services.ts b/src/services/changes/changes.list.services.ts new file mode 100644 index 00000000..9c9decd5 --- /dev/null +++ b/src/services/changes/changes.list.services.ts @@ -0,0 +1,53 @@ +import {fromNullable, nonNullish, toNullable, uint8ArrayToHexString} from '@dfinity/utils'; +import {listProposals} from '@junobuild/cdn'; +import {nextArg} from '@junobuild/cli-tools'; +import {junoConfigExist, readJunoConfig} from '../../configs/juno.config'; +import {configEnv} from '../../utils/config.utils'; +import {formatTime} from '../../utils/format.utils'; +import {consoleNoConfigFound} from '../../utils/msg.utils'; +import {satelliteParameters} from '../../utils/satellite.utils'; + +export const listChanges = async (args?: string[]) => { + if (!(await junoConfigExist())) { + consoleNoConfigFound(); + return; + } + + const env = configEnv(args); + const {satellite: satelliteConfig} = await readJunoConfig(env); + + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); + + const all = nextArg({args, option: '-a'}) ?? nextArg({args, option: '--all'}); + + const {items} = await listProposals({ + cdn: {satellite}, + filter: { + order: toNullable({ + desc: true + }), + paginate: toNullable() + } + }); + + const changes = items + .filter(([_, {status}]) => 'Open' in status) + .reduce((acc, [proposalId, {sha256, created_at}]) => { + const hash = fromNullable(sha256); + + return { + ...acc, + [`${proposalId}`]: { + hash: nonNullish(hash) ? uint8ArrayToHexString(hash) : '', + created_at: formatTime(new Date(Number(created_at / 1_000_000n))) + } + }; + }, {}); + + if (Object.keys(changes).length === 0) { + console.log('No changes open at the moment.'); + return; + } + + console.table(changes); +}; From 070596ce91f1497b3bbfdc3f0ff9ef5c7b0c950a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 17:31:44 +0200 Subject: [PATCH 03/16] feat: label --- src/services/changes/changes.list.services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/changes/changes.list.services.ts b/src/services/changes/changes.list.services.ts index 9c9decd5..4af481bf 100644 --- a/src/services/changes/changes.list.services.ts +++ b/src/services/changes/changes.list.services.ts @@ -45,7 +45,7 @@ export const listChanges = async (args?: string[]) => { }, {}); if (Object.keys(changes).length === 0) { - console.log('No changes open at the moment.'); + console.log('There are no open changes right now.'); return; } From da69860ff659aea39448c743f53f17858dd79c10 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 17:52:22 +0200 Subject: [PATCH 04/16] feat: wip --- src/services/changes/changes.list.services.ts | 62 +++++++++++++++---- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/src/services/changes/changes.list.services.ts b/src/services/changes/changes.list.services.ts index 4af481bf..d38da810 100644 --- a/src/services/changes/changes.list.services.ts +++ b/src/services/changes/changes.list.services.ts @@ -1,7 +1,9 @@ import {fromNullable, nonNullish, toNullable, uint8ArrayToHexString} from '@dfinity/utils'; -import {listProposals} from '@junobuild/cdn'; -import {nextArg} from '@junobuild/cli-tools'; +import {listProposals as listProposalsLib} from '@junobuild/cdn'; +import {Proposal} from '@junobuild/cdn'; +import {hasArgs} from '@junobuild/cli-tools'; import {junoConfigExist, readJunoConfig} from '../../configs/juno.config'; +import {SatelliteParametersWithId} from '../../types/satellite'; import {configEnv} from '../../utils/config.utils'; import {formatTime} from '../../utils/format.utils'; import {consoleNoConfigFound} from '../../utils/msg.utils'; @@ -18,22 +20,17 @@ export const listChanges = async (args?: string[]) => { const satellite = await satelliteParameters({satellite: satelliteConfig, env}); - const all = nextArg({args, option: '-a'}) ?? nextArg({args, option: '--all'}); + const all = hasArgs({args, options: ['-a', '--all']}); - const {items} = await listProposals({ - cdn: {satellite}, - filter: { - order: toNullable({ - desc: true - }), - paginate: toNullable() - } + const items = await listProposals({ + satellite, + traverseAll: all }); const changes = items .filter(([_, {status}]) => 'Open' in status) .reduce((acc, [proposalId, {sha256, created_at}]) => { - const hash = fromNullable(sha256); + const hash: Uint8Array | number[] | undefined = fromNullable(sha256); return { ...acc, @@ -51,3 +48,44 @@ export const listChanges = async (args?: string[]) => { console.table(changes); }; + +const listProposals = async ({ + startAfter, + satellite, + traverseAll +}: { + startAfter?: bigint; + satellite: SatelliteParametersWithId; + traverseAll: boolean; +}): Promise<[ProposalKey, Proposal][]> => { + const {items, items_length, matches_length} = await listProposalsLib({ + cdn: {satellite}, + filter: { + order: toNullable({ + desc: true + }), + paginate: nonNullish(startAfter) + ? toNullable({ + start_after: toNullable(startAfter), + limit: toNullable() + }) + : toNullable() + } + }); + + const last = (elements: T[]): T | undefined => { + const {length, [length - 1]: last} = elements; + return last; + }; + + if (items_length > matches_length && traverseAll) { + const nextItems = await listProposals({ + startAfter: last(items)?.[0].proposal_id, + satellite, + traverseAll + }); + return [...items, ...nextItems]; + } + + return items; +}; From 6a8afb27036d09dc2328d05db52b2fdf84aadff3 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 18:01:57 +0200 Subject: [PATCH 05/16] feat: import key --- src/services/changes/changes.list.services.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/changes/changes.list.services.ts b/src/services/changes/changes.list.services.ts index d38da810..c26804f2 100644 --- a/src/services/changes/changes.list.services.ts +++ b/src/services/changes/changes.list.services.ts @@ -1,6 +1,5 @@ import {fromNullable, nonNullish, toNullable, uint8ArrayToHexString} from '@dfinity/utils'; -import {listProposals as listProposalsLib} from '@junobuild/cdn'; -import {Proposal} from '@junobuild/cdn'; +import {listProposals as listProposalsLib, Proposal, ProposalKey} from '@junobuild/cdn'; import {hasArgs} from '@junobuild/cli-tools'; import {junoConfigExist, readJunoConfig} from '../../configs/juno.config'; import {SatelliteParametersWithId} from '../../types/satellite'; From 08b97069db9a46130d8aa89bfdccf6d4ca9f8b0e Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 20:53:30 +0200 Subject: [PATCH 06/16] feat: adapt --- src/services/changes/changes.list.services.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/services/changes/changes.list.services.ts b/src/services/changes/changes.list.services.ts index c26804f2..0f4105ce 100644 --- a/src/services/changes/changes.list.services.ts +++ b/src/services/changes/changes.list.services.ts @@ -1,23 +1,12 @@ import {fromNullable, nonNullish, toNullable, uint8ArrayToHexString} from '@dfinity/utils'; import {listProposals as listProposalsLib, Proposal, ProposalKey} from '@junobuild/cdn'; import {hasArgs} from '@junobuild/cli-tools'; -import {junoConfigExist, readJunoConfig} from '../../configs/juno.config'; import {SatelliteParametersWithId} from '../../types/satellite'; -import {configEnv} from '../../utils/config.utils'; import {formatTime} from '../../utils/format.utils'; -import {consoleNoConfigFound} from '../../utils/msg.utils'; -import {satelliteParameters} from '../../utils/satellite.utils'; +import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; export const listChanges = async (args?: string[]) => { - if (!(await junoConfigExist())) { - consoleNoConfigFound(); - return; - } - - const env = configEnv(args); - const {satellite: satelliteConfig} = await readJunoConfig(env); - - const satellite = await satelliteParameters({satellite: satelliteConfig, env}); + const {satellite} = await assertConfigAndLoadSatelliteContext(args); const all = hasArgs({args, options: ['-a', '--all']}); From 1894cfca02b9a4bc3eae18b75f345fec738c45d8 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 21:02:00 +0200 Subject: [PATCH 07/16] fix: proposal id as key --- src/services/changes/changes.list.services.ts | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/services/changes/changes.list.services.ts b/src/services/changes/changes.list.services.ts index 0f4105ce..d7466ca1 100644 --- a/src/services/changes/changes.list.services.ts +++ b/src/services/changes/changes.list.services.ts @@ -1,7 +1,7 @@ import {fromNullable, nonNullish, toNullable, uint8ArrayToHexString} from '@dfinity/utils'; -import {listProposals as listProposalsLib, Proposal, ProposalKey} from '@junobuild/cdn'; +import {listProposals as listProposalsLib, type Proposal, type ProposalKey} from '@junobuild/cdn'; import {hasArgs} from '@junobuild/cli-tools'; -import {SatelliteParametersWithId} from '../../types/satellite'; +import {type SatelliteParametersWithId} from '../../types/satellite'; import {formatTime} from '../../utils/format.utils'; import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; @@ -17,17 +17,20 @@ export const listChanges = async (args?: string[]) => { const changes = items .filter(([_, {status}]) => 'Open' in status) - .reduce((acc, [proposalId, {sha256, created_at}]) => { - const hash: Uint8Array | number[] | undefined = fromNullable(sha256); + .reduce>( + (acc, [{proposal_id}, {sha256, created_at}]) => { + const hash: Uint8Array | number[] | undefined = fromNullable(sha256); - return { - ...acc, - [`${proposalId}`]: { - hash: nonNullish(hash) ? uint8ArrayToHexString(hash) : '', - created_at: formatTime(new Date(Number(created_at / 1_000_000n))) - } - }; - }, {}); + return { + ...acc, + [`${proposal_id}`]: { + hash: nonNullish(hash) ? uint8ArrayToHexString(hash) : '', + created_at: formatTime(new Date(Number(created_at / 1_000_000n))) + } + }; + }, + {} + ); if (Object.keys(changes).length === 0) { console.log('There are no open changes right now.'); @@ -45,7 +48,7 @@ const listProposals = async ({ startAfter?: bigint; satellite: SatelliteParametersWithId; traverseAll: boolean; -}): Promise<[ProposalKey, Proposal][]> => { +}): Promise> => { const {items, items_length, matches_length} = await listProposalsLib({ cdn: {satellite}, filter: { From f38036d16271d0d4b6149640e3f827ec6fcc923a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 21:11:48 +0200 Subject: [PATCH 08/16] feat: apply changes --- src/commands/changes.ts | 3 +- .../changes/changes.apply.services.ts | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/services/changes/changes.apply.services.ts diff --git a/src/commands/changes.ts b/src/commands/changes.ts index 75409b42..a0adfb5e 100644 --- a/src/commands/changes.ts +++ b/src/commands/changes.ts @@ -3,6 +3,7 @@ import {logHelpChangesApply} from '../help/changes.apply.help'; import {logHelpChanges} from '../help/changes.help'; import {logHelpChangesList} from '../help/changes.list.help'; import {listChanges} from '../services/changes/changes.list.services'; +import {applyChanges} from '../services/changes/changes.apply.services'; export const changes = async (args?: string[]) => { const [subCommand] = args ?? []; @@ -12,7 +13,7 @@ export const changes = async (args?: string[]) => { await listChanges(args); break; case 'apply': - // TODO + await applyChanges(args); break; default: console.log(red('Unknown subcommand.')); diff --git a/src/services/changes/changes.apply.services.ts b/src/services/changes/changes.apply.services.ts new file mode 100644 index 00000000..21079810 --- /dev/null +++ b/src/services/changes/changes.apply.services.ts @@ -0,0 +1,40 @@ +import {assertNonNullish} from '@dfinity/utils'; +import {hexStringToUint8Array} from '@dfinity/utils/dist/types/utils/arrays.utils'; +import {commitProposal} from '@junobuild/cdn'; +import {nextArg} from '@junobuild/cli-tools'; +import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; + +export const applyChanges = async (args?: string[]) => { + const {satellite} = await assertConfigAndLoadSatelliteContext(args); + + const id = nextArg({args, option: '-i'}) ?? nextArg({args, option: '--id'}); + + assertNonNullish(id, 'An id must be provided'); + + const toBigInt = (): bigint => { + try { + return BigInt(id); + } catch (_err: unknown) { + console.error('The id must be a valid number.'); + process.exit(1); + } + }; + + const proposalId = toBigInt(); + + const hash = nextArg({args, option: '-s'}) ?? nextArg({args, option: '--hash'}); + + assertNonNullish(hash, 'A hash must be provided'); + + await commitProposal({ + cdn: { + satellite + }, + proposal: { + proposal_id: proposalId, + sha256: hexStringToUint8Array(hash) + } + }); + + console.log(`šŸŽÆ Change ${proposalId} applied.`); +}; From 0e52c4a5ed1b80074d8e748f3c7cdfe71e24ec23 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 30 May 2025 21:18:35 +0200 Subject: [PATCH 09/16] fix: auto import path --- src/commands/changes.ts | 2 +- src/services/changes/changes.apply.services.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/changes.ts b/src/commands/changes.ts index a0adfb5e..272d6546 100644 --- a/src/commands/changes.ts +++ b/src/commands/changes.ts @@ -2,8 +2,8 @@ import {red} from 'kleur'; import {logHelpChangesApply} from '../help/changes.apply.help'; import {logHelpChanges} from '../help/changes.help'; import {logHelpChangesList} from '../help/changes.list.help'; -import {listChanges} from '../services/changes/changes.list.services'; import {applyChanges} from '../services/changes/changes.apply.services'; +import {listChanges} from '../services/changes/changes.list.services'; export const changes = async (args?: string[]) => { const [subCommand] = args ?? []; diff --git a/src/services/changes/changes.apply.services.ts b/src/services/changes/changes.apply.services.ts index 21079810..9476e25b 100644 --- a/src/services/changes/changes.apply.services.ts +++ b/src/services/changes/changes.apply.services.ts @@ -1,5 +1,4 @@ -import {assertNonNullish} from '@dfinity/utils'; -import {hexStringToUint8Array} from '@dfinity/utils/dist/types/utils/arrays.utils'; +import {assertNonNullish, hexStringToUint8Array} from '@dfinity/utils'; import {commitProposal} from '@junobuild/cdn'; import {nextArg} from '@junobuild/cli-tools'; import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; From 6b4bfa9fbd1a3bb069f117dbf1ffc75b8c269392 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 31 May 2025 11:26:15 +0200 Subject: [PATCH 10/16] feat: format date --- src/services/changes/changes.list.services.ts | 4 ++-- src/utils/format.utils.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/services/changes/changes.list.services.ts b/src/services/changes/changes.list.services.ts index d7466ca1..2ffdbd0f 100644 --- a/src/services/changes/changes.list.services.ts +++ b/src/services/changes/changes.list.services.ts @@ -2,7 +2,7 @@ import {fromNullable, nonNullish, toNullable, uint8ArrayToHexString} from '@dfin import {listProposals as listProposalsLib, type Proposal, type ProposalKey} from '@junobuild/cdn'; import {hasArgs} from '@junobuild/cli-tools'; import {type SatelliteParametersWithId} from '../../types/satellite'; -import {formatTime} from '../../utils/format.utils'; +import {formatDate} from '../../utils/format.utils'; import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; export const listChanges = async (args?: string[]) => { @@ -25,7 +25,7 @@ export const listChanges = async (args?: string[]) => { ...acc, [`${proposal_id}`]: { hash: nonNullish(hash) ? uint8ArrayToHexString(hash) : '', - created_at: formatTime(new Date(Number(created_at / 1_000_000n))) + created_at: formatDate(new Date(Number(created_at / 1_000_000n))) } }; }, diff --git a/src/utils/format.utils.ts b/src/utils/format.utils.ts index 6620b25c..7e3c3b21 100644 --- a/src/utils/format.utils.ts +++ b/src/utils/format.utils.ts @@ -17,3 +17,14 @@ export const formatTime = (date: Date = new Date()): string => { hour12: false }).format(date); }; + +export const formatDate = (date: Date = new Date()): string => { + return new Intl.DateTimeFormat('en', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false + }).format(date); +}; From 2e00f052fa00fedf15baf0b0b3da2211aaa35952 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 31 May 2025 11:35:01 +0200 Subject: [PATCH 11/16] feat: reject proposal --- src/commands/changes.ts | 8 +++++ src/constants/help.constants.ts | 1 + src/help/changes.help.ts | 8 +++-- src/help/changes.reject.help.ts | 29 +++++++++++++++++++ .../changes/changes.apply.services.ts | 25 +++------------- .../changes/changes.reject.services.ts | 22 ++++++++++++++ src/utils/changes.utils.ts | 28 ++++++++++++++++++ 7 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 src/help/changes.reject.help.ts create mode 100644 src/services/changes/changes.reject.services.ts create mode 100644 src/utils/changes.utils.ts diff --git a/src/commands/changes.ts b/src/commands/changes.ts index 272d6546..21abe70b 100644 --- a/src/commands/changes.ts +++ b/src/commands/changes.ts @@ -2,8 +2,10 @@ import {red} from 'kleur'; import {logHelpChangesApply} from '../help/changes.apply.help'; import {logHelpChanges} from '../help/changes.help'; import {logHelpChangesList} from '../help/changes.list.help'; +import {logHelpChangesReject} from '../help/changes.reject.help'; import {applyChanges} from '../services/changes/changes.apply.services'; import {listChanges} from '../services/changes/changes.list.services'; +import {rejectChanges} from '../services/changes/changes.reject.services'; export const changes = async (args?: string[]) => { const [subCommand] = args ?? []; @@ -15,6 +17,9 @@ export const changes = async (args?: string[]) => { case 'apply': await applyChanges(args); break; + case 'reject': + await rejectChanges(args); + break; default: console.log(red('Unknown subcommand.')); logHelpChanges(); @@ -31,6 +36,9 @@ export const helpChanges = (args?: string[]) => { case 'apply': logHelpChangesApply(args); break; + case 'reject': + logHelpChangesReject(args); + break; default: logHelpChanges(args); } diff --git a/src/constants/help.constants.ts b/src/constants/help.constants.ts index 5ae7d98d..95b6cefe 100644 --- a/src/constants/help.constants.ts +++ b/src/constants/help.constants.ts @@ -34,3 +34,4 @@ export const DEV_BUILD_NOTES = `- If no language is provided, the CLI attempts t export const CHANGES_LIST_DESCRIPTION = 'List all submitted or applied changes.'; export const CHANGES_APPLY_DESCRIPTION = 'Apply a submitted change.'; +export const CHANGES_REJECT_DESCRIPTION = 'Reject a change.'; diff --git a/src/help/changes.help.ts b/src/help/changes.help.ts index 6216c3a4..3f3d8b13 100644 --- a/src/help/changes.help.ts +++ b/src/help/changes.help.ts @@ -2,20 +2,22 @@ import {cyan, green, magenta, yellow} from 'kleur'; import { CHANGES_APPLY_DESCRIPTION, CHANGES_DESCRIPTION, - CHANGES_LIST_DESCRIPTION + CHANGES_LIST_DESCRIPTION, + CHANGES_REJECT_DESCRIPTION } from '../constants/help.constants'; import {helpOutput} from './common.help'; import {TITLE} from './help'; const helpChangesList = `${magenta('list')} ${CHANGES_LIST_DESCRIPTION}`; - const helpChangesApply = `${magenta('apply')} ${CHANGES_APPLY_DESCRIPTION}`; +const helpChangesReject = `${magenta('reject')} ${CHANGES_REJECT_DESCRIPTION}`; const usage = `Usage: ${green('juno')} ${cyan('changes')} ${magenta('')} ${yellow('[options]')} Subcommands: ${helpChangesApply} - ${helpChangesList}`; + ${helpChangesList} + ${helpChangesReject}`; const doc = `${CHANGES_DESCRIPTION} diff --git a/src/help/changes.reject.help.ts b/src/help/changes.reject.help.ts new file mode 100644 index 00000000..f7ca41c4 --- /dev/null +++ b/src/help/changes.reject.help.ts @@ -0,0 +1,29 @@ +import {cyan, green, magenta, yellow} from 'kleur'; +import {CHANGES_REJECT_DESCRIPTION} from '../constants/help.constants'; +import {helpOutput} from './common.help'; +import {TITLE} from './help'; + +const usage = `Usage: ${green('juno')} ${cyan('changes')} ${magenta('reject')} ${yellow('[options]')} + +Options: + ${yellow('-i, --id')} The ID of the change to reject. + ${yellow('-s, --hash')} The expected hash of all included changes (for verification). + ${yellow('-h, --help')} Output usage information.`; + +const doc = `${CHANGES_REJECT_DESCRIPTION} + +\`\`\` +${usage} +\`\`\` +`; + +const help = `${TITLE} + +${CHANGES_REJECT_DESCRIPTION} + +${usage} +`; + +export const logHelpChangesReject = (args?: string[]) => { + console.log(helpOutput(args) === 'doc' ? doc : help); +}; diff --git a/src/services/changes/changes.apply.services.ts b/src/services/changes/changes.apply.services.ts index 9476e25b..d3c35c5e 100644 --- a/src/services/changes/changes.apply.services.ts +++ b/src/services/changes/changes.apply.services.ts @@ -1,29 +1,12 @@ -import {assertNonNullish, hexStringToUint8Array} from '@dfinity/utils'; +import {hexStringToUint8Array} from '@dfinity/utils'; import {commitProposal} from '@junobuild/cdn'; -import {nextArg} from '@junobuild/cli-tools'; +import {readChangesIdAndHash} from '../../utils/changes.utils'; import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; export const applyChanges = async (args?: string[]) => { const {satellite} = await assertConfigAndLoadSatelliteContext(args); - const id = nextArg({args, option: '-i'}) ?? nextArg({args, option: '--id'}); - - assertNonNullish(id, 'An id must be provided'); - - const toBigInt = (): bigint => { - try { - return BigInt(id); - } catch (_err: unknown) { - console.error('The id must be a valid number.'); - process.exit(1); - } - }; - - const proposalId = toBigInt(); - - const hash = nextArg({args, option: '-s'}) ?? nextArg({args, option: '--hash'}); - - assertNonNullish(hash, 'A hash must be provided'); + const {proposalId, hash} = readChangesIdAndHash(args); await commitProposal({ cdn: { @@ -35,5 +18,5 @@ export const applyChanges = async (args?: string[]) => { } }); - console.log(`šŸŽÆ Change ${proposalId} applied.`); + console.log(`šŸŽÆ Change ID ${proposalId} applied.`); }; diff --git a/src/services/changes/changes.reject.services.ts b/src/services/changes/changes.reject.services.ts new file mode 100644 index 00000000..de6839e1 --- /dev/null +++ b/src/services/changes/changes.reject.services.ts @@ -0,0 +1,22 @@ +import {hexStringToUint8Array} from '@dfinity/utils'; +import {rejectProposal} from '@junobuild/cdn'; +import {readChangesIdAndHash} from '../../utils/changes.utils'; +import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; + +export const rejectChanges = async (args?: string[]) => { + const {satellite} = await assertConfigAndLoadSatelliteContext(args); + + const {proposalId, hash} = readChangesIdAndHash(args); + + await rejectProposal({ + cdn: { + satellite + }, + proposal: { + proposal_id: proposalId, + sha256: hexStringToUint8Array(hash) + } + }); + + console.log(`🚫 Change ID ${proposalId} rejected.`); +}; diff --git a/src/utils/changes.utils.ts b/src/utils/changes.utils.ts new file mode 100644 index 00000000..bbe1fb81 --- /dev/null +++ b/src/utils/changes.utils.ts @@ -0,0 +1,28 @@ +import {assertNonNullish} from '@dfinity/utils'; +import {nextArg} from '@junobuild/cli-tools'; + +export const readChangesIdAndHash = (args?: string[]): {proposalId: bigint; hash: string} => { + const id = nextArg({args, option: '-i'}) ?? nextArg({args, option: '--id'}); + + assertNonNullish(id, 'An id must be provided'); + + const toBigInt = (): bigint => { + try { + return BigInt(id); + } catch (_err: unknown) { + console.error('The id must be a valid number.'); + process.exit(1); + } + }; + + const proposalId = toBigInt(); + + const hash = nextArg({args, option: '-s'}) ?? nextArg({args, option: '--hash'}); + + assertNonNullish(hash, 'A hash must be provided'); + + return { + proposalId, + hash + }; +}; From aeca84bf8f8eac6ce21a466440a82631fe8a8031 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 31 May 2025 12:32:55 +0200 Subject: [PATCH 12/16] feat: list every --- src/help/changes.list.help.ts | 1 + src/services/changes/changes.list.services.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/help/changes.list.help.ts b/src/help/changes.list.help.ts index 5bd586c3..69cf217b 100644 --- a/src/help/changes.list.help.ts +++ b/src/help/changes.list.help.ts @@ -7,6 +7,7 @@ const usage = `Usage: ${green('juno')} ${cyan('changes')} ${magenta('list')} ${y Options: ${yellow('-a, --all')} Search through all changes, not just the 100 most recent. + ${yellow('-e, --every')} Include changes of any status (default is only submitted ones). ${yellow('-h, --help')} Output usage information.`; const doc = `${CHANGES_LIST_DESCRIPTION} diff --git a/src/services/changes/changes.list.services.ts b/src/services/changes/changes.list.services.ts index 2ffdbd0f..18f5acf5 100644 --- a/src/services/changes/changes.list.services.ts +++ b/src/services/changes/changes.list.services.ts @@ -9,6 +9,7 @@ export const listChanges = async (args?: string[]) => { const {satellite} = await assertConfigAndLoadSatelliteContext(args); const all = hasArgs({args, options: ['-a', '--all']}); + const every = hasArgs({args, options: ['-e', '--every']}); const items = await listProposals({ satellite, @@ -16,7 +17,7 @@ export const listChanges = async (args?: string[]) => { }); const changes = items - .filter(([_, {status}]) => 'Open' in status) + .filter(([_, {status}]) => 'Open' in status || every) .reduce>( (acc, [{proposal_id}, {sha256, created_at}]) => { const hash: Uint8Array | number[] | undefined = fromNullable(sha256); From 31013b6ab72832f704bb547519edb8e45f147b84 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 31 May 2025 12:56:39 +0200 Subject: [PATCH 13/16] feat: list on files on clear --- src/commands/deploy.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 0022dd49..0938200c 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -44,7 +44,10 @@ const deployWithProposal = async ({args, clearOption}: {args?: string[]; clearOp satellite }: DeployFnParams): Promise => await cliDeployWithProposal({ - deploy, + deploy: { + ...deploy, + includeAllFiles: clearOption + }, proposal: { clearAssets: clearOption, autoCommit: !noCommit, From a836f5c2dc3410af147b630fa71064eea0475381 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 31 May 2025 13:01:27 +0200 Subject: [PATCH 14/16] feat: spinner --- .../changes/changes.apply.services.ts | 27 ++++++++++++------- .../changes/changes.reject.services.ts | 27 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/services/changes/changes.apply.services.ts b/src/services/changes/changes.apply.services.ts index d3c35c5e..21cd96dd 100644 --- a/src/services/changes/changes.apply.services.ts +++ b/src/services/changes/changes.apply.services.ts @@ -1,5 +1,6 @@ import {hexStringToUint8Array} from '@dfinity/utils'; import {commitProposal} from '@junobuild/cdn'; +import ora from 'ora'; import {readChangesIdAndHash} from '../../utils/changes.utils'; import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; @@ -8,15 +9,21 @@ export const applyChanges = async (args?: string[]) => { const {proposalId, hash} = readChangesIdAndHash(args); - await commitProposal({ - cdn: { - satellite - }, - proposal: { - proposal_id: proposalId, - sha256: hexStringToUint8Array(hash) - } - }); + const spinner = ora('Applying...').start(); - console.log(`šŸŽÆ Change ID ${proposalId} applied.`); + try { + await commitProposal({ + cdn: { + satellite + }, + proposal: { + proposal_id: proposalId, + sha256: hexStringToUint8Array(hash) + } + }); + + console.log(`šŸŽÆ Change ID ${proposalId} applied.`); + } finally { + spinner.stop(); + } }; diff --git a/src/services/changes/changes.reject.services.ts b/src/services/changes/changes.reject.services.ts index de6839e1..4ffb804b 100644 --- a/src/services/changes/changes.reject.services.ts +++ b/src/services/changes/changes.reject.services.ts @@ -1,5 +1,6 @@ import {hexStringToUint8Array} from '@dfinity/utils'; import {rejectProposal} from '@junobuild/cdn'; +import ora from 'ora'; import {readChangesIdAndHash} from '../../utils/changes.utils'; import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; @@ -8,15 +9,21 @@ export const rejectChanges = async (args?: string[]) => { const {proposalId, hash} = readChangesIdAndHash(args); - await rejectProposal({ - cdn: { - satellite - }, - proposal: { - proposal_id: proposalId, - sha256: hexStringToUint8Array(hash) - } - }); + const spinner = ora('Rejecting...').start(); - console.log(`🚫 Change ID ${proposalId} rejected.`); + try { + await rejectProposal({ + cdn: { + satellite + }, + proposal: { + proposal_id: proposalId, + sha256: hexStringToUint8Array(hash) + } + }); + + console.log(`🚫 Change ID ${proposalId} rejected.`); + } finally { + spinner.stop(); + } }; From bc2fc0f022c9f0bae856eb7947b6280fd002d6da Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 31 May 2025 13:05:30 +0200 Subject: [PATCH 15/16] feat: rename --no-commit into --no-apply --- src/commands/deploy.ts | 2 +- src/help/deploy.help.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 0938200c..fb547555 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -37,7 +37,7 @@ export const deploy = async (args?: string[]) => { }; const deployWithProposal = async ({args, clearOption}: {args?: string[]; clearOption: boolean}) => { - const noCommit = hasArgs({args, options: ['-n', '--no-commit']}); + const noCommit = hasArgs({args, options: ['-n', '--no-apply']}); const deployFn = async ({ deploy, diff --git a/src/help/deploy.help.ts b/src/help/deploy.help.ts index c66ef431..f8ae3070 100644 --- a/src/help/deploy.help.ts +++ b/src/help/deploy.help.ts @@ -7,7 +7,7 @@ const usage = `Usage: ${green('juno')} ${cyan('deploy')} ${yellow('[options]')} Options: ${yellow('-c, --clear')} Clear existing app files before proceeding with deployment. - ${yellow('-n, --no-commit')} Submit the deployment as a change but do not apply it yet. + ${yellow('-n, --no-apply')} Submit the deployment as a change but do not apply it yet. ${yellow('-i, --immediate')} Deploy files instantly (bypasses the change workflow). ${helpMode} ${yellow('-h, --help')} Output usage information.`; From 0cf6faab7a43d92f268ae82c1ff8394df0e2265b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 31 May 2025 13:11:05 +0200 Subject: [PATCH 16/16] feat: print on new line because of spinner --- src/services/changes/changes.apply.services.ts | 2 +- src/services/changes/changes.reject.services.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/changes/changes.apply.services.ts b/src/services/changes/changes.apply.services.ts index 21cd96dd..e4c92912 100644 --- a/src/services/changes/changes.apply.services.ts +++ b/src/services/changes/changes.apply.services.ts @@ -22,7 +22,7 @@ export const applyChanges = async (args?: string[]) => { } }); - console.log(`šŸŽÆ Change ID ${proposalId} applied.`); + console.log(`\nšŸŽÆ Change ID ${proposalId} applied.`); } finally { spinner.stop(); } diff --git a/src/services/changes/changes.reject.services.ts b/src/services/changes/changes.reject.services.ts index 4ffb804b..d63b40c7 100644 --- a/src/services/changes/changes.reject.services.ts +++ b/src/services/changes/changes.reject.services.ts @@ -22,7 +22,7 @@ export const rejectChanges = async (args?: string[]) => { } }); - console.log(`🚫 Change ID ${proposalId} rejected.`); + console.log(`\n🚫 Change ID ${proposalId} rejected.`); } finally { spinner.stop(); }