incwo-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api.ts ADDED
@@ -0,0 +1,93 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { parseStringPromise, Builder } from 'xml2js';
3
+ import { IncwoConfig, baseUrl } from './config';
4
+
5
+ export class IncwoClient {
6
+ private http: AxiosInstance;
7
+ private base: string;
8
+
9
+ constructor(config: IncwoConfig) {
10
+ this.base = baseUrl(config);
11
+ this.http = axios.create({
12
+ baseURL: this.base,
13
+ auth: { username: config.login, password: config.password },
14
+ headers: { 'Content-Type': 'application/xml', 'Accept': 'application/xml' },
15
+ timeout: 15000,
16
+ });
17
+ }
18
+
19
+ // GET /{resource}.xml with optional query params
20
+ async list(resource: string, params: Record<string, string> = {}): Promise<any[]> {
21
+ const response = await this.http.get(`/${resource}.xml`, { params });
22
+ return parseXmlList(response.data);
23
+ }
24
+
25
+ // GET /{resource}/{id}.xml
26
+ async get(resource: string, id: string): Promise<any> {
27
+ const response = await this.http.get(`/${resource}/${id}.xml`);
28
+ const parsed = await parseXmlList(response.data);
29
+ return parsed[0] ?? null;
30
+ }
31
+
32
+ // POST /{resource}.xml — accepts a plain JS object, converts to XML
33
+ async create(resource: string, data: Record<string, any>): Promise<any> {
34
+ const singularResource = toSingular(resource);
35
+ const xml = jsonToXml(singularResource, data);
36
+ const response = await this.http.post(`/${resource}.xml`, xml);
37
+ const parsed = await parseXmlList(response.data);
38
+ return parsed[0] ?? null;
39
+ }
40
+
41
+ // PUT /{resource}/{id}.xml — accepts a plain JS object, converts to XML
42
+ async update(resource: string, id: string, data: Record<string, any>): Promise<any> {
43
+ const singularResource = toSingular(resource);
44
+ const xml = jsonToXml(singularResource, data);
45
+ const response = await this.http.put(`/${resource}/${id}.xml`, xml);
46
+ const parsed = await parseXmlList(response.data);
47
+ return parsed[0] ?? null;
48
+ }
49
+
50
+ // DELETE /{resource}/{id}.xml
51
+ async destroy(resource: string, id: string): Promise<void> {
52
+ await this.http.delete(`/${resource}/${id}.xml`);
53
+ }
54
+ }
55
+
56
+ // Converts a flat JS object to an XML string with the given root element
57
+ // e.g. jsonToXml('contact', { first_name: 'Bob' }) → <contact><first_name>Bob</first_name></contact>
58
+ function jsonToXml(rootElement: string, data: Record<string, any>): string {
59
+ const builder = new Builder({ rootName: rootElement, headless: true });
60
+ return builder.buildObject(data);
61
+ }
62
+
63
+ // Naive singularization: bill_sheets → bill_sheet, contacts → contact
64
+ function toSingular(resource: string): string {
65
+ if (resource.endsWith('ies')) return resource.slice(0, -3) + 'y';
66
+ if (resource.endsWith('s')) return resource.slice(0, -1);
67
+ return resource;
68
+ }
69
+
70
+ async function parseXmlList(xml: string): Promise<any[]> {
71
+ const parsed = await parseStringPromise(xml, {
72
+ explicitArray: false,
73
+ ignoreAttrs: true,
74
+ trim: true,
75
+ });
76
+
77
+ if (!parsed) return [];
78
+
79
+ const rootKey = Object.keys(parsed)[0];
80
+ const root = parsed[rootKey];
81
+
82
+ if (!root) return [];
83
+
84
+ const childKey = Object.keys(root)[0];
85
+ if (childKey && Array.isArray(root[childKey])) {
86
+ return root[childKey];
87
+ }
88
+ if (childKey && typeof root[childKey] === 'object') {
89
+ return [root[childKey]];
90
+ }
91
+
92
+ return [root];
93
+ }
@@ -0,0 +1,156 @@
1
+ import { Command } from 'commander';
2
+ import ora from 'ora';
3
+ import { loadConfig } from '../config';
4
+ import { IncwoClient } from '../api';
5
+ import { printTable, printObject, error, success } from '../ui';
6
+ import { ResourceDef } from '../resources';
7
+
8
+ export function makeCommand(res: ResourceDef): Command {
9
+ const cmd = new Command(res.command).description(res.description);
10
+
11
+ // ── list ──────────────────────────────────────────────────────────────────
12
+ const listCmd = cmd
13
+ .command('list')
14
+ .description(`List ${res.description.toLowerCase()}`)
15
+ .option('--page <n>', 'Page number', '1')
16
+ .option('--search <q>', 'Full-text search')
17
+ .option('--filter <json>', 'ufilters JSON (e.g. \'{"status":{"eq":"active"}}\')');
18
+
19
+ if (res.hasSheetType) {
20
+ listCmd.option('--sheet-type <type>', 'Filter by sheet type (proposal, sale_order, delivery_note, purchase_order…)');
21
+ }
22
+
23
+ if (res.hasDateFilter) {
24
+ listCmd
25
+ .option('--from <date>', 'Start date (YYYY-MM-DD)')
26
+ .option('--to <date>', 'End date (YYYY-MM-DD)');
27
+ }
28
+
29
+ listCmd.action(async (opts) => {
30
+ const config = loadConfig();
31
+ if (!config) return error('No configuration found. Run: incwo config');
32
+
33
+ const spinner = ora('Loading…').start();
34
+ try {
35
+ const params: Record<string, string> = {
36
+ page: opts.page,
37
+ ...res.defaultParams,
38
+ };
39
+ if (opts.search) params['search'] = opts.search;
40
+ if (opts.filter) params['ufilters'] = opts.filter;
41
+ if (opts.sheetType) params['sheet_type'] = opts.sheetType;
42
+ if (opts.from) params['filter_from_date'] = opts.from;
43
+ if (opts.to) params['filter_to_date'] = opts.to;
44
+
45
+ const client = new IncwoClient(config);
46
+ const rows = await client.list(res.endpoint, params);
47
+ spinner.stop();
48
+ printTable(rows, res.columns);
49
+ } catch (e: any) {
50
+ spinner.stop();
51
+ error(e.response?.data ?? e.message);
52
+ }
53
+ });
54
+
55
+ // ── get ───────────────────────────────────────────────────────────────────
56
+ cmd
57
+ .command('get <id>')
58
+ .description(`Show a ${res.description.toLowerCase()} by ID`)
59
+ .action(async (id) => {
60
+ const config = loadConfig();
61
+ if (!config) return error('No configuration found. Run: incwo config');
62
+
63
+ const spinner = ora('Loading…').start();
64
+ try {
65
+ const client = new IncwoClient(config);
66
+ const obj = await client.get(res.endpoint, id);
67
+ spinner.stop();
68
+ if (!obj) return error(`${res.command} #${id} not found`);
69
+ printObject(obj, `${res.command} #${id}`);
70
+ } catch (e: any) {
71
+ spinner.stop();
72
+ error(e.response?.data ?? e.message);
73
+ }
74
+ });
75
+
76
+ // ── create ────────────────────────────────────────────────────────────────
77
+ cmd
78
+ .command('create')
79
+ .description(`Create a new ${res.description.toLowerCase()}`)
80
+ .requiredOption('--data <json>', 'Fields as JSON (e.g. \'{"first_name":"Bob","last_name":"Smith"}\')')
81
+ .action(async (opts) => {
82
+ const config = loadConfig();
83
+ if (!config) return error('No configuration found. Run: incwo config');
84
+
85
+ let data: Record<string, any>;
86
+ try {
87
+ data = JSON.parse(opts.data);
88
+ } catch {
89
+ return error('Invalid JSON in --data');
90
+ }
91
+
92
+ const spinner = ora('Creating…').start();
93
+ try {
94
+ const client = new IncwoClient(config);
95
+ const obj = await client.create(res.endpoint, data);
96
+ spinner.stop();
97
+ if (!obj) return error('Created but no response returned');
98
+ success(`${res.command} created (id: ${obj.id ?? '?'})`);
99
+ printObject(obj);
100
+ } catch (e: any) {
101
+ spinner.stop();
102
+ error(e.response?.data ?? e.message);
103
+ }
104
+ });
105
+
106
+ // ── update ────────────────────────────────────────────────────────────────
107
+ cmd
108
+ .command('update <id>')
109
+ .description(`Update a ${res.description.toLowerCase()}`)
110
+ .requiredOption('--data <json>', 'Fields to update as JSON (e.g. \'{"first_name":"Bob"}\')')
111
+ .action(async (id, opts) => {
112
+ const config = loadConfig();
113
+ if (!config) return error('No configuration found. Run: incwo config');
114
+
115
+ let data: Record<string, any>;
116
+ try {
117
+ data = JSON.parse(opts.data);
118
+ } catch {
119
+ return error('Invalid JSON in --data');
120
+ }
121
+
122
+ const spinner = ora('Updating…').start();
123
+ try {
124
+ const client = new IncwoClient(config);
125
+ const obj = await client.update(res.endpoint, id, data);
126
+ spinner.stop();
127
+ success(`${res.command} #${id} updated`);
128
+ if (obj) printObject(obj);
129
+ } catch (e: any) {
130
+ spinner.stop();
131
+ error(e.response?.data ?? e.message);
132
+ }
133
+ });
134
+
135
+ // ── delete ────────────────────────────────────────────────────────────────
136
+ cmd
137
+ .command('delete <id>')
138
+ .description(`Delete a ${res.description.toLowerCase()}`)
139
+ .action(async (id) => {
140
+ const config = loadConfig();
141
+ if (!config) return error('No configuration found. Run: incwo config');
142
+
143
+ const spinner = ora('Deleting…').start();
144
+ try {
145
+ const client = new IncwoClient(config);
146
+ await client.destroy(res.endpoint, id);
147
+ spinner.stop();
148
+ success(`${res.command} #${id} deleted`);
149
+ } catch (e: any) {
150
+ spinner.stop();
151
+ error(e.response?.data ?? e.message);
152
+ }
153
+ });
154
+
155
+ return cmd;
156
+ }
package/src/config.ts ADDED
@@ -0,0 +1,118 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import inquirer from 'inquirer';
5
+
6
+ export interface IncwoConfig {
7
+ server: string; // e.g. "mycompany.incwo.com" or "www.incwo.com"
8
+ business_file_id: string;
9
+ login: string;
10
+ password: string;
11
+ }
12
+
13
+ const CONFIG_DIR = path.join(os.homedir(), '.incwo');
14
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
15
+
16
+ export function loadConfig(): IncwoConfig | null {
17
+ if (!fs.existsSync(CONFIG_PATH)) return null;
18
+ try {
19
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
20
+ return JSON.parse(raw) as IncwoConfig;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ export function saveConfig(config: IncwoConfig): void {
27
+ if (!fs.existsSync(CONFIG_DIR)) {
28
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
29
+ }
30
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
31
+ }
32
+
33
+ export function baseUrl(config: IncwoConfig): string {
34
+ return `https://${config.server}/${config.business_file_id}`;
35
+ }
36
+
37
+ export async function runConfigWizard(): Promise<void> {
38
+ const chalk = (await import('chalk')).default;
39
+
40
+ console.log(chalk.bold('\nincwo CLI — Configuration\n'));
41
+
42
+ const { inputMode } = await inquirer.prompt([
43
+ {
44
+ type: 'list',
45
+ name: 'inputMode',
46
+ message: 'How would you like to configure the server?',
47
+ choices: [
48
+ { name: 'Enter the full endpoint URL (e.g. mycompany.incwo.com/12345)', value: 'url' },
49
+ { name: 'Enter the shard and business file ID separately', value: 'manual' },
50
+ ],
51
+ },
52
+ ]);
53
+
54
+ let server: string;
55
+ let business_file_id: string;
56
+
57
+ if (inputMode === 'url') {
58
+ const { endpoint } = await inquirer.prompt([
59
+ {
60
+ type: 'input',
61
+ name: 'endpoint',
62
+ message: 'Endpoint URL (e.g. mycompany.incwo.com/12345 or www.incwo.com/12345):',
63
+ validate: (v: string) => {
64
+ const clean = v.replace(/^https?:\/\//, '').replace(/\/$/, '');
65
+ const parts = clean.split('/');
66
+ if (parts.length < 2 || !parts[0] || !parts[1]) {
67
+ return 'Expected format: shard.incwo.com/business_file_id';
68
+ }
69
+ return true;
70
+ },
71
+ },
72
+ ]);
73
+ const clean = endpoint.replace(/^https?:\/\//, '').replace(/\/$/, '');
74
+ const slashIdx = clean.indexOf('/');
75
+ server = clean.substring(0, slashIdx);
76
+ business_file_id = clean.substring(slashIdx + 1).split('/')[0];
77
+ } else {
78
+ const answers = await inquirer.prompt([
79
+ {
80
+ type: 'input',
81
+ name: 'shard',
82
+ message: 'Shard (leave empty if no shard → www.incwo.com):',
83
+ },
84
+ {
85
+ type: 'input',
86
+ name: 'business_file_id',
87
+ message: 'Business file ID:',
88
+ validate: (v: string) => v.trim() !== '' || 'Required',
89
+ },
90
+ ]);
91
+ server = answers.shard.trim()
92
+ ? `${answers.shard.trim()}.incwo.com`
93
+ : 'www.incwo.com';
94
+ business_file_id = answers.business_file_id.trim();
95
+ }
96
+
97
+ const { login, password } = await inquirer.prompt([
98
+ {
99
+ type: 'input',
100
+ name: 'login',
101
+ message: 'Login:',
102
+ validate: (v: string) => v.trim() !== '' || 'Required',
103
+ },
104
+ {
105
+ type: 'password',
106
+ name: 'password',
107
+ message: 'Password:',
108
+ mask: '*',
109
+ validate: (v: string) => v.trim() !== '' || 'Required',
110
+ },
111
+ ]);
112
+
113
+ const config: IncwoConfig = { server, business_file_id, login, password };
114
+ saveConfig(config);
115
+
116
+ console.log(chalk.green(`\n✓ Configuration saved to ${CONFIG_PATH}`));
117
+ console.log(chalk.dim(` Endpoint: https://${server}/${business_file_id}\n`));
118
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { printBanner } from './ui';
4
+ import { runConfigWizard } from './config';
5
+ import { makeCommand } from './commands/generic';
6
+ import { RESOURCES } from './resources';
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('incwo')
12
+ .description('CLI for incwo CRM/ERP')
13
+ .version('0.1.0');
14
+
15
+ // Banner sur --help global ou sans arguments
16
+ if (process.argv.length <= 2 || process.argv[2] === '--help' || process.argv[2] === '-h') {
17
+ printBanner();
18
+ }
19
+
20
+ // incwo config
21
+ program
22
+ .command('config')
23
+ .description('Configure incwo access (server, credentials)')
24
+ .action(async () => {
25
+ await runConfigWizard();
26
+ });
27
+
28
+ // Toutes les ressources via le factory générique
29
+ for (const resource of RESOURCES) {
30
+ program.addCommand(makeCommand(resource));
31
+ }
32
+
33
+ program.parse(process.argv);