@nzz/q-cli 2.0.6 → 2.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.
package/README.md CHANGED
@@ -41,7 +41,7 @@ Example:
41
41
  Creates a new custom code project.
42
42
 
43
43
  ```bash
44
- Q new-custom-code -d my-custom-code-project
44
+ Q new-custom-code my-custom-code-project
45
45
  ```
46
46
 
47
47
  ### Creating a new custom code item
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nzz/q-cli",
3
- "version": "2.0.6",
3
+ "version": "2.1.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -1,41 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "Q Config",
4
- "description": "Config used by the Q CLI to update items",
5
- "type": "object",
6
- "properties": {
7
- "items": {
8
- "description": "Array of Q items",
9
- "type": "array",
10
- "minItems": 1,
11
- "items": {
12
- "type": "object",
13
- "properties": {
14
- "environments": {
15
- "type": "array",
16
- "minItems": 1,
17
- "items": {
18
- "type": "object",
19
- "properties": {
20
- "name": {
21
- "type": "string",
22
- "description": "Environment name of Q item"
23
- },
24
- "id": {
25
- "type": "string",
26
- "description": "Id of Q item"
27
- }
28
- },
29
- "required": ["name", "id"]
30
- }
31
- },
32
- "item": {
33
- "type": "object"
34
- }
35
- },
36
- "required": ["environments", "item"]
37
- }
38
- }
39
- },
40
- "required": ["items"]
41
- }
@@ -1,74 +0,0 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- import { logSuccess, logError, updateItem, getAccessToken, getQServerUrl } from './utils.js';
11
- import { readFileSync, existsSync, writeFileSync } from 'fs';
12
- import { resolve } from 'path';
13
- export default function (command) {
14
- return __awaiter(this, void 0, void 0, function* () {
15
- const qConfigPath = resolve(command.config);
16
- if (!existsSync(qConfigPath)) {
17
- logError(`Couldn't find config file: '${qConfigPath}'.\nCreate a config file in the current directory or pass the path to the config file with the option -c <path>`);
18
- process.exit(1);
19
- }
20
- if (!command.environment) {
21
- logError('Environment is required');
22
- process.exit(1);
23
- }
24
- // Get the Q server URL and access token from the environment variables or 1Password
25
- const qServerUrl = getQServerUrl(command.environment);
26
- const accessToken = getAccessToken(command.environment);
27
- const newCustomCodeDoc = {
28
- acronym: 'Visuals',
29
- assetGroups: [],
30
- createdDate: '', // Will be set when saved
31
- createdBy: 'editorial-tech@nzz.ch',
32
- data: [],
33
- // @ts-ignore
34
- department: 'Visuals',
35
- files: [],
36
- options: {
37
- previewDisabled: true,
38
- },
39
- // @ts-ignore
40
- publication: 'nzz',
41
- sophieModules: [],
42
- title: command.title,
43
- // @ts-ignore
44
- tool: 'custom_code',
45
- updatedDate: '', // Will be set when saved
46
- updatedBy: 'editorial-tech@nzz.ch',
47
- };
48
- // Create the item on the Q server
49
- const result = yield updateItem(newCustomCodeDoc, qServerUrl, accessToken);
50
- if (!result.success) {
51
- logError(result.msg);
52
- process.exit(1);
53
- }
54
- const newCustomCodeDocId = result.id;
55
- // Update the config file with the new item
56
- const qConfig = JSON.parse(readFileSync(qConfigPath, 'utf-8'));
57
- if (!qConfig.items) {
58
- qConfig.items = [];
59
- }
60
- if (qConfig.items.length === 0) {
61
- qConfig.items.push({
62
- environments: [],
63
- item: {}, // User needs to add the item later
64
- });
65
- }
66
- const firstItem = qConfig.items[0];
67
- firstItem.environments.push({
68
- id: newCustomCodeDocId,
69
- name: command.environment,
70
- });
71
- writeFileSync(qConfigPath, JSON.stringify(qConfig, null, 2));
72
- logSuccess(`Successfully created item with id ${newCustomCodeDocId} on ${command.environment} environment`);
73
- });
74
- }
package/dist/enums.js DELETED
@@ -1,7 +0,0 @@
1
- export var Environment;
2
- (function (Environment) {
3
- Environment["PRODUCTION"] = "production";
4
- Environment["STAGING"] = "staging";
5
- Environment["TEST"] = "test";
6
- Environment["LOCAL"] = "local";
7
- })(Environment || (Environment = {}));
package/dist/index.js DELETED
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env node
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- import { Command } from 'commander';
12
- import packageJson from '../package.json' with { type: 'json' };
13
- import updateItem from './updateItem.js';
14
- import newCustomCode from './newCustomCode.js';
15
- import createCustomCodeItem from './createCustomCodeItem.js';
16
- const program = new Command();
17
- // Get the version from the package.json file
18
- const version = packageJson.version;
19
- function main() {
20
- return __awaiter(this, void 0, void 0, function* () {
21
- program.version(version).description('Q Toolbox cli');
22
- program
23
- .command('new-custom-code')
24
- .argument('<path>', 'the base directory to bootstrap the new custom code project in')
25
- .description('bootstrap a new custom code project')
26
- .action(() => {
27
- const dir = program.args[1];
28
- newCustomCode({ dir });
29
- });
30
- program
31
- .command('update-item')
32
- .description('update q item')
33
- .option('-c, --config [path]', 'set config path which defines the q items to be updated. defaults to ./q.config.json', `${process.cwd()}/q.config.json`)
34
- .option('-e, --environment [env]', 'set environment which should be updated, defaults to update all items of all environments defined in config')
35
- .action((command) => __awaiter(this, void 0, void 0, function* () {
36
- yield updateItem(command);
37
- }));
38
- program
39
- .command('create-custom-code-item')
40
- .description('creates a new q custom code item in the db and adds it to the q config file')
41
- .option('-c, --config [path]', 'set config path to q.config.json. defaults to ./q.config.json', `${process.cwd()}/q.config.json`)
42
- .option('-e, --environment [env]', 'set environment where the new q custom code item should be created in')
43
- .option('-t, --title [title]', 'set title of the new q custom code item')
44
- .action((command) => __awaiter(this, void 0, void 0, function* () {
45
- yield createCustomCodeItem(command);
46
- }));
47
- yield program.parseAsync(process.argv);
48
- });
49
- }
50
- main();
@@ -1 +0,0 @@
1
- export {};
@@ -1,107 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { execSync } from 'child_process';
4
- import { logError, logSuccess } from './utils.js';
5
- export default function (command) {
6
- const { dir } = command;
7
- if (!dir) {
8
- logError('No custom-code project name/directory given');
9
- process.exit(1);
10
- }
11
- if (fs.existsSync(dir)) {
12
- logError(`Directory ${dir} already exists`);
13
- process.exit(1);
14
- }
15
- const skeletonDir = 'skeletons';
16
- const skeletonCustomCodeDir = path.join(skeletonDir, 'custom-code-project');
17
- try {
18
- // Create the target directory
19
- fs.mkdirSync(dir);
20
- // Change to the target directory
21
- process.chdir(dir);
22
- // Initialize git repository to pull the skeletons/custom-code-project content into a separate directory
23
- console.log('Initializing git repository...');
24
- execSync('git init');
25
- execSync('git remote add origin https://github.com/nzzdev/Q.git');
26
- // Enable sparse checkout and pull the content from remote
27
- console.log(`Sparse checkout ${skeletonCustomCodeDir}...`);
28
- execSync('git sparse-checkout init --cone');
29
- execSync(`git sparse-checkout set ${skeletonCustomCodeDir}`);
30
- console.log('Pulling content from remote...');
31
- execSync('git pull origin main');
32
- // Remove all files, except the skeletons directory
33
- console.log('Cleaning up...');
34
- const cwdFiles = fs.readdirSync(process.cwd());
35
- for (const file of cwdFiles) {
36
- if (file === skeletonDir)
37
- continue; // Skip the skeletons directory
38
- const filePath = path.join(process.cwd(), file);
39
- if (fs.statSync(filePath).isDirectory()) {
40
- fs.rmSync(filePath, { recursive: true, force: true });
41
- }
42
- else {
43
- fs.unlinkSync(filePath);
44
- }
45
- }
46
- // Copy the skeletons/custom-code-project content to the root
47
- const cwdSkeletonCustomCodeDir = path.join(process.cwd(), skeletonCustomCodeDir);
48
- const skeletonFiles = fs.readdirSync(cwdSkeletonCustomCodeDir);
49
- for (const file of skeletonFiles) {
50
- const srcFilePath = path.join(cwdSkeletonCustomCodeDir, file);
51
- const targetFilePath = path.join(process.cwd(), file);
52
- if (fs.statSync(srcFilePath).isDirectory()) {
53
- fs.cpSync(srcFilePath, targetFilePath, { recursive: true });
54
- }
55
- else {
56
- fs.copyFileSync(srcFilePath, targetFilePath);
57
- }
58
- }
59
- // Remove the now empty skeletons directory
60
- fs.rmSync(path.join(process.cwd(), skeletonDir), { recursive: true, force: true });
61
- // Update the package.json and q.config.json files with the custom code project name
62
- const customCodeProjectName = path.basename(dir);
63
- updateJsonValue(path.join(process.cwd(), 'package.json'), 'name', customCodeProjectName);
64
- updateJsonValue(path.join(process.cwd(), 'q.config.json'), 'items.0.item.trackingComponent.componentInfo.componentName', customCodeProjectName);
65
- logSuccess(`Successfully created new custom code project '${dir}'`);
66
- }
67
- catch (error) {
68
- logError(error.message);
69
- process.exit(1);
70
- }
71
- }
72
- /**
73
- * Reads a JSON file, updates a specific value, and writes the changes back to the file
74
- * @param filePath - Path to the JSON file
75
- * @param keyPath - Path to the key to update (use dot notation for nested objects, e.g., 'parent.child')
76
- * @param newValue - New value to set
77
- * @returns boolean indicating success
78
- */
79
- function updateJsonValue(filePath, keyPath, newValue) {
80
- try {
81
- // Read the JSON file
82
- const jsonContent = JSON.parse(fs.readFileSync(filePath, 'utf8'));
83
- // Split the key path into parts for nested objects
84
- const keys = keyPath.split('.');
85
- let current = jsonContent;
86
- // Navigate to the parent of the target property
87
- for (let i = 0; i < keys.length - 1; i++) {
88
- if (!(keys[i] in current)) {
89
- throw new Error(`Key path ${keyPath} not found in JSON file`);
90
- }
91
- current = current[keys[i]];
92
- }
93
- // Update the value
94
- const lastKey = keys[keys.length - 1];
95
- if (!(lastKey in current)) {
96
- throw new Error(`Key ${lastKey} not found in JSON file`);
97
- }
98
- current[lastKey] = newValue;
99
- // Write the updated JSON back to the file
100
- fs.writeFileSync(filePath, JSON.stringify(jsonContent, null, 2));
101
- return true;
102
- }
103
- catch (error) {
104
- logError('Error updating JSON file:' + error.message);
105
- return false;
106
- }
107
- }
@@ -1,184 +0,0 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- import Ajv from 'ajv';
11
- import { readFileSync, existsSync, createReadStream } from 'fs';
12
- import { resolve } from 'path';
13
- import FormData from 'form-data';
14
- import fetch from 'node-fetch'; // Use node-fetch instead of native fetch, because native fetch seems to have issues with FormData/uploading files
15
- import { getAccessToken, getQServerUrl, logError, logSuccess, updateItem } from './utils.js';
16
- import updateSchemaJson from './assets/updateSchema.json' with { type: 'json' };
17
- import { Environment } from './enums.js';
18
- const envCredentials = {
19
- [Environment.LOCAL]: {
20
- qServerUrl: '',
21
- accessToken: '',
22
- },
23
- [Environment.TEST]: {
24
- qServerUrl: '',
25
- accessToken: '',
26
- },
27
- [Environment.STAGING]: {
28
- qServerUrl: '',
29
- accessToken: '',
30
- },
31
- [Environment.PRODUCTION]: {
32
- qServerUrl: '',
33
- accessToken: '',
34
- },
35
- };
36
- /**
37
- * Get the Q server URL and access token from the environment variables or 1Password
38
- * @param environment The environment to get the credentials for
39
- * @returns The Q server URL and access token
40
- */
41
- function getEnvCredentials(environment) {
42
- if (envCredentials[environment].qServerUrl === '' || envCredentials[environment].accessToken === '') {
43
- envCredentials[environment] = {
44
- qServerUrl: getQServerUrl(environment),
45
- accessToken: getAccessToken(environment),
46
- };
47
- }
48
- return envCredentials[environment];
49
- }
50
- function validateConfig(metaSchema, qConfig) {
51
- const ajv = new Ajv({ allErrors: true });
52
- let isValid = true;
53
- let errorsText = '';
54
- try {
55
- isValid = ajv.validate(metaSchema, qConfig);
56
- errorsText = ajv.errorsText();
57
- }
58
- catch (error) {
59
- isValid = false;
60
- errorsText = `Error occurred while validating the config file: ${error.message}`;
61
- }
62
- return {
63
- isValid,
64
- errorsText,
65
- };
66
- }
67
- function uploadFile(path, qServerUrl, accessToken) {
68
- return __awaiter(this, void 0, void 0, function* () {
69
- const localFilePath = resolve(path);
70
- // Check if the file exists
71
- if (!existsSync(localFilePath)) {
72
- logError(`File not found: ${localFilePath}`);
73
- return undefined;
74
- }
75
- try {
76
- const form = new FormData();
77
- // Read the file
78
- const fileStream = createReadStream(localFilePath);
79
- // Append the file to the form
80
- form.append('file', fileStream);
81
- // Upload the file to the server
82
- const response = yield fetch(`${qServerUrl}/file`, {
83
- method: 'POST',
84
- body: form,
85
- headers: Object.assign(Object.assign({}, form.getHeaders()), { 'user-agent': 'Q Command-line Tool', Authorization: `Bearer ${accessToken}` }),
86
- });
87
- const data = (yield response.json());
88
- if (!data.apiResponseStatus.success) {
89
- logError(`Failed to upload file: ${data.apiResponseStatus.msg}`);
90
- return undefined;
91
- }
92
- logSuccess(`Successfully uploaded file: ${localFilePath}`);
93
- return data.fileInfo;
94
- }
95
- catch (error) {
96
- logError(`Error uploading file: ${error instanceof Error ? error.message : String(error)}`);
97
- }
98
- });
99
- }
100
- /**
101
- * Recursively processes a document structure, looking for 'path' properties
102
- * and processing them with the processResource function.
103
- *
104
- * @param item The object or value to process
105
- * @param processResource Function to handle path properties
106
- * @returns The processed item
107
- */
108
- function processPathProperties(item, processResource) {
109
- return __awaiter(this, void 0, void 0, function* () {
110
- // Base case: if item is null or not an object, return it unchanged
111
- if (item === null || typeof item !== 'object') {
112
- return item;
113
- }
114
- // Handle arrays
115
- if (Array.isArray(item)) {
116
- return yield Promise.all(item.map((element) => __awaiter(this, void 0, void 0, function* () { return yield processPathProperties(element, processResource); })));
117
- }
118
- // Check if this object has a 'path' property (using Object.prototype.hasOwnProperty.call to avoid prototype pollution)
119
- if (Object.prototype.hasOwnProperty.call(item, 'path') && typeof item.path === 'string') {
120
- return yield processResource(item.path);
121
- }
122
- // Process each property recursively
123
- const result = Object.assign({}, item);
124
- for (const key of Object.keys(item)) {
125
- result[key] = yield processPathProperties(item[key], processResource);
126
- }
127
- return result;
128
- });
129
- }
130
- export default function (command) {
131
- return __awaiter(this, void 0, void 0, function* () {
132
- const qConfigPath = resolve(command.config);
133
- if (!existsSync(qConfigPath)) {
134
- logError(`Couldn't find config file: '${qConfigPath}'.\nCreate a config file in the current directory or pass the path to the config file with the option -c <path>`);
135
- return;
136
- }
137
- const qConfig = JSON.parse(readFileSync(qConfigPath, 'utf-8'));
138
- const validationResult = validateConfig(updateSchemaJson, qConfig); // Validate the config file against the schema
139
- if (!validationResult.isValid) {
140
- logError(`A problem occurred while validating the config file: ${validationResult.errorsText}`);
141
- process.exit(1);
142
- }
143
- let qServerUrl;
144
- let accessToken;
145
- let environmentToUpdate;
146
- for (const item of qConfig.items) {
147
- for (const environment of item.environments) {
148
- // If the environment is specified, use that environment
149
- if (command.environment) {
150
- // Skip if the environment is not the one we want to update
151
- if (command.environment !== environment.name)
152
- continue;
153
- environmentToUpdate = command.environment;
154
- }
155
- else {
156
- // Otherwise, use the environment from the config
157
- environmentToUpdate = environment.name;
158
- }
159
- // Get the credentials for the current environment
160
- const credentials = getEnvCredentials(environmentToUpdate);
161
- qServerUrl = credentials.qServerUrl;
162
- accessToken = credentials.accessToken;
163
- console.log(`Updating item ${environment.id} on ${environmentToUpdate} environment (${qServerUrl})...`);
164
- let qDoc = structuredClone(item.item);
165
- qDoc._id = environment.id;
166
- // Process the 'path' properties and upload files
167
- qDoc = yield processPathProperties(qDoc, (path) => __awaiter(this, void 0, void 0, function* () {
168
- const formattedPath = path.replace('{id}', environment.id);
169
- console.log(`Processing resource at path: ${formattedPath}`);
170
- return yield uploadFile(formattedPath, qServerUrl, accessToken);
171
- }));
172
- // Update the item on the Q server
173
- const result = yield updateItem(qDoc, qServerUrl, accessToken);
174
- if (result.success) {
175
- logSuccess(`Successfully updated item with id ${environment.id} on ${environmentToUpdate} environment`);
176
- }
177
- else {
178
- logError(result.msg);
179
- process.exit(1);
180
- }
181
- }
182
- }
183
- });
184
- }
package/dist/utils.js DELETED
@@ -1,88 +0,0 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- import { read } from '@1password/op-js';
11
- import fetch from 'node-fetch';
12
- const colors = {
13
- green: '\x1b[32m',
14
- reset: '\x1b[0m',
15
- red: '\x1b[31m',
16
- };
17
- export function getAccessToken(env) {
18
- return process.env[`Q_${env.toUpperCase()}_ACCESSTOKEN`]
19
- ? process.env[`Q_${env.toUpperCase()}_ACCESSTOKEN`]
20
- : readSecret(`op://Q CLI/qv2-${env.toLowerCase()}/CLI_AUTH_TOKEN`);
21
- }
22
- export function getQServerUrl(env) {
23
- return process.env[`Q_${env.toUpperCase()}_SERVER`]
24
- ? process.env[`Q_${env.toUpperCase()}_SERVER`]
25
- : readSecret(`op://Q CLI/qv2-${env.toLowerCase()}/Q_SERVER_BASE_URL`);
26
- }
27
- export function logError(message) {
28
- console.error(`${colors.red}${message}${colors.reset}`);
29
- }
30
- export function logSuccess(message) {
31
- console.log(`${colors.green}${message}${colors.reset}`);
32
- }
33
- export function updateItem(qDoc, qServerUrl, accessToken) {
34
- return __awaiter(this, void 0, void 0, function* () {
35
- const retVal = {
36
- id: undefined,
37
- msg: '',
38
- success: false,
39
- };
40
- let data;
41
- try {
42
- const response = yield fetch(`${qServerUrl}/item`, {
43
- method: 'POST',
44
- body: JSON.stringify(qDoc),
45
- headers: {
46
- 'user-agent': 'Q Command-line Tool',
47
- Authorization: `Bearer ${accessToken}`,
48
- 'Content-Type': 'application/json',
49
- },
50
- });
51
- data = (yield response.json());
52
- }
53
- catch (error) {
54
- if (error instanceof TypeError && error.message.includes('fetch failed')) {
55
- retVal.msg =
56
- 'Network request failed: Unable to connect to the server. Please check your internet connection and server status.';
57
- }
58
- else {
59
- retVal.msg = `Failed to update item: ${error instanceof Error ? error.message : String(error)}`;
60
- }
61
- return retVal;
62
- }
63
- if (isAuthError(data)) {
64
- retVal.msg = `Authentication error: ${data.error}`;
65
- return retVal;
66
- }
67
- if (!data.success) {
68
- retVal.msg = `Error occurred while updating item: ${data.msg}`;
69
- return retVal;
70
- }
71
- retVal.success = true;
72
- retVal.id = data.id; // If the item was created, we get the id from the response
73
- return retVal;
74
- });
75
- }
76
- // Use type predicate to check response type
77
- function isAuthError(data) {
78
- return 'error' in data;
79
- }
80
- function readSecret(reference) {
81
- try {
82
- return read.parse(reference);
83
- }
84
- catch (error) {
85
- logError(`Error occurred while reading the secret: ${error.message}`);
86
- process.exit(1);
87
- }
88
- }