@inizioevoke/veeva-astroclm-core 1.0.2 → 1.0.4

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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: inizioevoke-veeva-astroclm
3
+ description: >
4
+ Skills and tools for working in this Veeva CLM Astro project (inizioevoke-veeva-astroclm).
5
+ Use this skill for any project-specific tasks such as creating pages, managing content, or
6
+ running project utilities. Triggers on any request that involves scaffolding, generating,
7
+ or managing project files in this Veeva CLM project.
8
+ ---
9
+
10
+ # inizioevoke-veeva-astroclm Skills
11
+
12
+ ## Available skills
13
+
14
+ - **Page Manager** — Create a new page in the project → read `page-manager/SKILL.md`
15
+ - **Veeva Config Manager** — Manage Veeva CLM configuration → read `veeva-config-manager/SKILL.md`
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: inizioevoke-veeva-astroclm-pagemanager
3
+ description: >
4
+ Scaffolds a new page in this Veeva CLM Astro project. Use this skill whenever the user asks to
5
+ create a new page, add a page, scaffold a slide, generate a new route, or set up a new CLM slide —
6
+ even if they don't say "page manager" explicitly. If the user is working in this project and wants
7
+ a new page, this skill should always trigger.
8
+ ---
9
+
10
+ # Page Manager
11
+
12
+ ## Step 1 — Ask for the page path
13
+
14
+ Ask: "What should the page path be? (relative to `src/contents/`, e.g. `home` or `module-1/slide-2`)"
15
+
16
+ ## Step 2 — Ask for the layout
17
+
18
+ Read `src/layouts/` to discover available layouts (each subdirectory containing a `.astro` file is a layout). Present them as a numbered list, for example:
19
+
20
+ ```
21
+ Which layout would you like to use?
22
+ 1. GlobalLayout
23
+ 2. LeftNavLayout
24
+ 3. TopNavLayout
25
+ ```
26
+
27
+ The user can respond with just a number. Resolve it to the layout name (e.g. `TopNavLayout`) before proceeding.
28
+
29
+ ## Step 3 — Check for veeva-config.ts
30
+
31
+ Check if `veeva-config.ts` exists in the project root. If it does, ask the user: "Would you like to add a slide entry for this page in `veeva-config.ts`?"
32
+
33
+ ## Step 4 — Create files
34
+
35
+ Given the page path (e.g. `this/one`):
36
+ - `contentName` = last segment of the path (e.g. `one`)
37
+ - `pageName` = full path with `/` replaced by `-` (e.g. `this-one`)
38
+
39
+ Check that neither `src/pages/<pageName>.astro` nor `src/contents/<path>/` already exist — if either does, stop and tell the user.
40
+
41
+ **Write `src/pages/<pageName>.astro`:**
42
+ ```astro
43
+ ---
44
+ import Page from '@/contents/<path>/<contentName>.astro';
45
+ ---
46
+ <Page />
47
+ ```
48
+
49
+ **Create `src/contents/<path>/` and write `<contentName>.astro`:**
50
+ ```astro
51
+ ---
52
+ import <layoutName> from '@/layouts/<layoutName>/<layoutName>.astro';
53
+
54
+ import './<contentName>.scss';
55
+ ---
56
+ <<layoutName>>
57
+ <!-- Insert page content here -->
58
+
59
+ <script>
60
+ import './<contentName>';
61
+ </script>
62
+ </<layoutName>>
63
+ ```
64
+
65
+ **Write `src/contents/<path>/<contentName>.scss`:** empty file
66
+
67
+ **Write `src/contents/<path>/<contentName>.ts`:** empty file
68
+
69
+ ## Step 5 — Add slide to veeva-config.ts
70
+
71
+ If the user said yes in Step 3, add a new slide entry to `presentation.slides` following the pattern in `../veeva-config-manager/manage.md` (Step 3 — Resolve missing slides).
72
+
73
+ ## Step 6 — Report
74
+
75
+ Tell the user which files were created.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: inizioevoke-veeva-astroclm-veeva-config-manager
3
+ description: >
4
+ Manages Veeva CLM configuration for this project. Use this skill whenever the user asks to
5
+ create, generate, or set up a Veeva config, initialize a veeva-config.ts, scaffold a new
6
+ presentation, or run the veeva-config-manager. Also trigger when the user says things like
7
+ "set up the config", "create the config file", "initialize the presentation config",
8
+ "sync the config", "check the config", or "update the slides in the config".
9
+ ---
10
+
11
+ # Veeva Config Manager
12
+
13
+ ## Available skills
14
+
15
+ - **Create config** — Generate a `veeva-config.ts` file for the project → read `create.md`
16
+ - **Manage config** — Sync `veeva-config.ts` slides with `src/pages/` → read `manage.md`
@@ -0,0 +1,154 @@
1
+ # veeva-create-config
2
+
3
+ Interactively collect presentation metadata from the user and write a
4
+ `veeva-config.ts` file to the project root, replicating the flow in
5
+ `src/apps/veeva-config-manager/create.ts`.
6
+
7
+ ## What you need from the user
8
+
9
+ Gather **all** of the following before writing any files. Ask for them
10
+ conversationally, batching related questions into a single message to minimize
11
+ round-trips:
12
+
13
+ | Field | Options / constraints |
14
+ |---|---|
15
+ | **Product name** | Free text, required |
16
+ | **Presentation name** | Free text, required |
17
+ | **Presentation ID** | Free text; default = presentation name lowercased, non-alphanumeric → `_` |
18
+ | **IVA dimensions** | `1024 × 768`, `1366 × 1024` *(default)*, `1376 × 1032` |
19
+ | **iOS resolution** | `Default For Device` *(default)*, `Scale To Fit`, `Scale To 1024x768` |
20
+ | **Disabled actions** | Multi-select, none required: History Buttons, Navigation Bar, Pinch to Exit, Reactions, Rotation Lock, Swipe, Zoom |
21
+ | **CRM target** | `Salesforce` *(default)*, `Vault` |
22
+
23
+ ## Generating IDs
24
+
25
+ You will need two kinds of random IDs. Implement them inline with JavaScript/TypeScript logic or generate them yourself:
26
+
27
+ - **External ID prefix**: First 3 chars of `product` (uppercased) + `_` + an 8-char alphanumeric random string (uppercase A–Z, 0–9).
28
+ Example: product `Velorixin` → `VEL_3K9BW12X`
29
+ - **Per-slide ID**: An independent 8-char alphanumeric random string for each slide, unique within the set.
30
+
31
+ To generate an 8-char random-ish ID in a bash snippet you can run:
32
+ ```bash
33
+ node -e "const a='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';let r='';for(let i=0;i<8;i++)r+=a[Math.floor(Math.random()*36)];console.log(r)"
34
+ ```
35
+ Or generate them yourself — just make them look like `3K9BW12X` (8 uppercase alphanumeric chars).
36
+
37
+ ## Discovering slides
38
+
39
+ Read all `.astro` files in `src/pages/`:
40
+
41
+ ```bash
42
+ ls src/pages/*.astro 2>/dev/null
43
+ ```
44
+
45
+ For each file `some-page-name.astro`:
46
+ - `path` = filename without `.astro` extension (e.g. `some-page-name`)
47
+ - `title` = title-case the path by splitting on `-`, capitalising each part, joining with space (e.g. `Some Page Name`)
48
+ - `externalId` = `formatExtId('<SLIDE_ID>')` — keep as a function call literal in the output. Ensure that `<SLIDE_ID>` is unique
49
+
50
+ ## Output format
51
+
52
+ Write `veeva-config.ts` to the **project root** using exactly this template
53
+ (replace all `##...##` tokens with the collected/generated values):
54
+
55
+ ```ts
56
+ import type { IVeevaClmBinderFields, IVeevaConfig, VeevaEnv } from "@inizioevoke/veeva-astroclm-core/types";
57
+ import { formatExtId as __formatExtId } from '@inizioevoke/veeva-astroclm-core/lib';
58
+
59
+ interface IVeevaConfigArgs {
60
+ veevaEnv?: VeevaEnv;
61
+ isTraining?: boolean;
62
+ }
63
+ export default function ({ veevaEnv = 'client', isTraining = false }: IVeevaConfigArgs = {}): IVeevaConfig {
64
+ const isEvokeBuild = veevaEnv === 'evoke';
65
+ const EXT_ID = `##EXTERNAL_ID##${isTraining ? '_TRN' : ''}`;
66
+ const PRESENTATION_NAME = '##PRESENTATION_NAME##';
67
+
68
+ const formatExtId = (id: string) => {
69
+ return __formatExtId(EXT_ID, id);
70
+ };
71
+
72
+ const binderFields: IVeevaClmBinderFields = {
73
+ // language: isEvokeBuild ? undefined : 'English',
74
+ // crmOrg: isEvokeBuild ? undefined : { crmId: ['CRM_ORD_ID'] },
75
+ // crmProduct: isEvokeBuild ? undefined : { id: 'VEEVA_ID_FROM_URL' },
76
+ // crmDetailGroup: isEvokeBuild ? undefined : { id: 'VEEVA_ID_FROM_URL' },
77
+ // detailGroup: undefined
78
+ };
79
+
80
+ return {
81
+ dimensions: { width: ##DIMENSIONS_WIDTH##, height: ##DIMENSIONS_HEIGHT## },
82
+ thumbnails: {
83
+ default: {
84
+ queryStringParams: {
85
+ screenshots: true
86
+ }
87
+ }
88
+ },
89
+ presentation: {
90
+ externalId: EXT_ID,
91
+ presentationId: `##PRESENTATION_ID##${isTraining ? '_trn' : ''}`,
92
+ product: '##PRODUCT##',
93
+ name: `${PRESENTATION_NAME}${isTraining ? ' [Training]' : ''}`,
94
+ country: 'United States',
95
+ isTraining,
96
+ crmTarget: '##CRM_TARGET##',
97
+ ...binderFields,
98
+
99
+ shared: {
100
+ externalId: formatExtId('SHARED'),
101
+ ...binderFields
102
+ },
103
+
104
+ slideSettings: {
105
+ mediaType: 'HTML',
106
+ disableActions: ##DISABLE_ACTIONS##,
107
+ iosResolution: '##IOS_RESOLUTION##',
108
+ ...binderFields
109
+ },
110
+ slides: ##SLIDES##
111
+ }
112
+ };
113
+ }
114
+
115
+ ```
116
+
117
+ ### Token substitution reference
118
+
119
+ | Token | Value |
120
+ |---|---|
121
+ | `##EXTERNAL_ID##` | e.g. `OZE_3K9BW12X` |
122
+ | `##PRESENTATION_NAME##` | cleaned presentation name (trim, collapse double spaces) |
123
+ | `##PRESENTATION_ID##` | cleaned presentation ID |
124
+ | `##PRODUCT##` | cleaned product name |
125
+ | `##DIMENSIONS_WIDTH##` | numeric width, no quotes (e.g. `1366`) |
126
+ | `##DIMENSIONS_HEIGHT##` | numeric height, no quotes (e.g. `1024`) |
127
+ | `##IOS_RESOLUTION##` | string value as-is (e.g. `Default For Device`) |
128
+ | `##DISABLE_ACTIONS##` | TS array literal e.g. `['Swipe', 'Zoom']` or `[]` |
129
+ | `##CRM_TARGET##` | `Salesforce` or `Vault` |
130
+ | `##SLIDES##` | Array of slide objects (see below) |
131
+
132
+ ### Slides array format
133
+
134
+ ```ts
135
+ [{
136
+ path: 'some-page-name',
137
+ title: 'Some Page Name',
138
+ externalId: formatExtId('3K9BW12X')
139
+ }, {
140
+ path: 'another-page',
141
+ title: 'Another Page',
142
+ externalId: formatExtId('7XZ2M4QR')
143
+ }]
144
+ ```
145
+
146
+ ## "Clean input" rule
147
+
148
+ Before using any free-text value in the output: `.trim()` and replace runs of
149
+ two or more spaces with a single space. Do not otherwise transform user input.
150
+
151
+ ## After writing the file
152
+
153
+ Tell the user where the file was written (`veeva-config.ts` in the project
154
+ root) and list the slides that were detected.
@@ -0,0 +1,48 @@
1
+ # veeva-manage-config
2
+
3
+ Audit `veeva-config.ts` against `src/pages/` to keep slides in sync with the actual pages in the project.
4
+
5
+ ## Step 1 — Discover pages and slides
6
+
7
+ Run in parallel:
8
+
9
+ 1. List all `.astro` files in `src/pages/`:
10
+ ```bash
11
+ ls src/pages/*.astro 2>/dev/null
12
+ ```
13
+ Extract the `path` for each: filename without the `.astro` extension (e.g. `some-page-name`).
14
+
15
+ 2. Read `veeva-config.ts` from the project root and extract the `path` property from each entry in `presentation.slides`.
16
+
17
+ ## Step 2 — Compare
18
+
19
+ Identify:
20
+ - **Missing slides** — pages in `src/pages/` that have no matching entry in `slides`
21
+ - **Extraneous slides** — entries in `slides` whose `path` has no matching file in `src/pages/`
22
+
23
+ If everything is in sync, tell the user and stop.
24
+
25
+ ## Step 3 — Resolve missing slides
26
+
27
+ For each missing slide, ask the user: "No slide entry found for `<path>`. Add it to `veeva-config.ts`?"
28
+
29
+ If yes, add a new slide object to the `slides` array following the same pattern from `create.md`:
30
+ - `path` = the page filename without `.astro`
31
+ - `title` = title-case by splitting on `-`, capitalising each part, joining with a space
32
+ - `externalId` = `formatExtId('<SLIDE_ID>')` where `<SLIDE_ID>` is a new unique 8-char uppercase alphanumeric string, different from all existing slide IDs in the file
33
+
34
+ Generate the ID with:
35
+ ```bash
36
+ node -e "const a='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';let r='';for(let i=0;i<8;i++)r+=a[Math.floor(Math.random()*36)];console.log(r)"
37
+ ```
38
+
39
+ ## Step 4 — Resolve extraneous slides
40
+
41
+ For each extraneous slide, ask the user: "Slide entry `<path>` has no matching page. Remove it or comment it out?"
42
+
43
+ - **Remove** — delete the slide object from the array entirely
44
+ - **Comment out** — wrap the slide object in a block comment: `/* { path: '...', ... } */`
45
+
46
+ ## Step 5 — Write the file
47
+
48
+ If any changes were made, write the updated `veeva-config.ts` and tell the user what was added, removed, or commented out.
@@ -1,5 +1 @@
1
- export * from './const.js';
2
- export { default as loadEnv } from './env.js';
3
- export { default as parseArgv } from './parse-argv.js';
4
- export { default as parseEnv } from './parse-env.js';
5
1
  export * from './utils.js';
package/dist/lib/index.js CHANGED
@@ -1,5 +1,2 @@
1
- export * from './const.js';
2
- export { default as loadEnv } from './env.js';
3
- export { default as parseArgv } from './parse-argv.js';
4
- export { default as parseEnv } from './parse-env.js';
1
+ // only export client-side compatible scripts
5
2
  export * from './utils.js';
@@ -0,0 +1,5 @@
1
+ export * from './const.js';
2
+ export { default as loadEnv } from './env.js';
3
+ export { default as parseArgv } from './parse-argv.js';
4
+ export { default as parseEnv } from './parse-env.js';
5
+ export * from './utils.js';
@@ -0,0 +1,5 @@
1
+ export * from './const.js';
2
+ export { default as loadEnv } from './env.js';
3
+ export { default as parseArgv } from './parse-argv.js';
4
+ export { default as parseEnv } from './parse-env.js';
5
+ export * from './utils.js';
@@ -1,14 +1,18 @@
1
1
  import type { IVeevaConfig } from '../types';
2
+ export interface AdditionalField {
3
+ header: string;
4
+ value?: string;
5
+ property?: string;
6
+ }
2
7
  interface Params {
3
8
  config: IVeevaConfig;
4
9
  deployDir: string;
5
10
  buildName: string;
6
11
  fieldsOnly?: boolean;
7
12
  additional?: {
8
- headers: string[];
9
- presentation: string[];
10
- slides: string[][];
11
- shared: string[];
13
+ presentation: AdditionalField[];
14
+ slides: AdditionalField[];
15
+ shared: AdditionalField[];
12
16
  };
13
17
  }
14
18
  export default function ({ config, deployDir, buildName, fieldsOnly, additional }: Params): Promise<void>;
@@ -4,7 +4,30 @@ import logger from '../lib/logger.js';
4
4
  import { pathToSlideZip } from '../lib/utils.js';
5
5
  export default async function ({ config, deployDir, buildName, fieldsOnly = false, additional }) {
6
6
  logger.info('Creating multichannel loader CSV');
7
- const headers = (fieldsOnly ? CSV_HEADERS_FIELDS_ONLY : CSV_HEADERS).concat(additional?.headers ?? []);
7
+ const headers = fieldsOnly ? CSV_HEADERS_FIELDS_ONLY : CSV_HEADERS;
8
+ additional?.presentation?.forEach((a) => {
9
+ if (a.value || (a.property && config.presentation[a.property])) {
10
+ headers.push(a.header);
11
+ }
12
+ });
13
+ additional?.slides?.forEach((a) => {
14
+ if (a.value) {
15
+ headers.push(a.header);
16
+ }
17
+ else if (a.property && config.presentation.slides) {
18
+ const slide = config.presentation.slides.find((s) => {
19
+ return s[a.property];
20
+ });
21
+ if (slide) {
22
+ headers.push(a.header);
23
+ }
24
+ }
25
+ });
26
+ additional?.shared?.forEach((a) => {
27
+ if (a.value || (a.property && config.presentation.shared && config.presentation.shared[a.property])) {
28
+ headers.push(a.header);
29
+ }
30
+ });
8
31
  const presentation = config.presentation;
9
32
  for (const [key, header] of Object.entries(BINDER_PRES_HEADERS)) {
10
33
  if (presentation[key]) {
@@ -17,12 +40,12 @@ export default async function ({ config, deployDir, buildName, fieldsOnly = fals
17
40
  }
18
41
  }
19
42
  const rows = [headers];
20
- rows.push(createPresentationRow(headers, config).concat(additional?.presentation ?? []));
43
+ rows.push(createPresentationRow(headers, config, additional?.presentation));
21
44
  for (let i = 0; i < config.presentation.slides.length; i++) {
22
- rows.push(createSlideRow(headers, config, config.presentation.slides[i]).concat(additional?.slides?.[i] ?? []));
45
+ rows.push(createSlideRow(headers, config, config.presentation.slides[i], additional?.slides));
23
46
  }
24
- const shared = createSharedRow(headers, config);
25
- shared && rows.push(shared.concat(additional?.shared ?? []));
47
+ const shared = createSharedRow(headers, config, additional?.shared);
48
+ shared && rows.push(shared);
26
49
  await writeFile(join(deployDir, `_${config.presentation.presentationId}_${buildName}.csv`), rows.map((r) => { return r.join(','); }).join('\n'), { encoding: 'utf8' });
27
50
  }
28
51
  const VEEVA_YES = 'YES', VEEVA_NO = 'NO', VEEVA_TYPE_PRESENTATION = 'Presentation', VEEVA_TYPE_SLIDE = 'Slide', VEEVA_TYPE_SHARED = 'Shared', VEEVA_LIFECYCLE_BINDER = 'Binder Lifecycle', VEEVA_LIFECYCLE_CRM_CONTENT = 'CRM Content Lifecycle', VEEVA_IOS_RESOLUTION_DEFAULT = 'Default For Device', VEEVA_MEDIATYPE_HTML = 'HTML',
@@ -30,10 +53,10 @@ const VEEVA_YES = 'YES', VEEVA_NO = 'NO', VEEVA_TYPE_PRESENTATION = 'Presentatio
30
53
  HEADER_FIELDS_ONLY = 'Fields Only', HEADER_EXTERNAL_ID = 'external_id__v', HEADER_NAME = 'name__v', HEADER_TYPE = 'Type', HEADER_LIFECYCLE = 'lifecycle__v', HEADER_PRES_ID = 'pres.crm_presentation_id__v', HEADER_PRES_TITLE = 'pres.title__v', HEADER_PRES_PRODUCT = 'pres.product__v.name__v', HEADER_PRES_COUNTRY = 'pres.country__v.name__v', HEADER_PRES_LANGUAGE = 'pres.language__v', HEADER_PRES_CLM_CONTENT = 'pres.clm_content__v', HEADER_PRES_CLM_HIDDEN = 'pres.crm_hidden__v', HEADER_PRES_TRAINING = 'pres.crm_training__v', HEADER_PRES_CRM_TARGET = 'pres.crm_target_platform__v', HEADER_PRES_CRM_ORG = 'pres.crm_org__v.crm_org_id__v', HEADER_PRES_CRM_PRODUCT = 'pres.crm_product__v.id', // Get the Veeva ID from the URL
31
54
  HEADER_PRES_CRM_CONTENT_GROUP = 'pres.crm_content_group__v.id', // Get the Veeva ID from the URL
32
55
  HEADER_PRES_CRM_DETAIL_GROUP = 'pres.crm_detail_group__v.id', // Get the Veeva ID from the URL
33
- HEADER_PRES_DETAIL_GROUP = 'pres.detail_group__v', HEADER_PRESENTATION = 'Presentation Link', HEADER_SLIDE_TITLE = 'slide.title__v', HEADER_SLIDE_PRODUCT = 'slide.product__v.name__v', HEADER_SLIDE_COUNTRY = 'slide.country__v.name__v', HEADER_SLIDE_LANGUAGE = 'slide.language__v', HEADER_SLIDE_MEDIA_TYPE = 'slide.crm_media_type__v', HEADER_SLIDE_IOS_RESOLUTION = 'slide.ios_resolution__v', HEADER_SLIDE_RELATED_SHARED = 'slide.related_shared_resource__v', HEADER_SLIDE_CLM_CONTENT = 'slide.clm_content__v', HEADER_SLIDE_FILE_NAME = 'slide.filename', HEADER_SLIDE_REACTIONS = 'slide.crm_custom_reaction__v', HEADER_SLIDE_DISABLE_ACTIONS = 'slide.crm_disable_actions__v', HEADER_SLIDE_CRM_TARGET = 'slide.crm_target_platform__v', HEADER_SLIDE_CRM_ORG = 'slide.crm_org__v.crm_org_id__v', HEADER_SLIDE_CRM_PRODUCT = 'slide.crm_product__v.id', // Get the Veeva ID from the URL
56
+ HEADER_PRES_DETAIL_GROUP = 'pres.detail_group__v', HEADER_PRES_EXP_DATE = 'pres.expiration_date__v', HEADER_PRESENTATION = 'Presentation Link', HEADER_SLIDE_TITLE = 'slide.title__v', HEADER_SLIDE_PRODUCT = 'slide.product__v.name__v', HEADER_SLIDE_COUNTRY = 'slide.country__v.name__v', HEADER_SLIDE_LANGUAGE = 'slide.language__v', HEADER_SLIDE_MEDIA_TYPE = 'slide.crm_media_type__v', HEADER_SLIDE_IOS_RESOLUTION = 'slide.ios_resolution__v', HEADER_SLIDE_RELATED_SHARED = 'slide.related_shared_resource__v', HEADER_SLIDE_CLM_CONTENT = 'slide.clm_content__v', HEADER_SLIDE_FILE_NAME = 'slide.filename', HEADER_SLIDE_REACTIONS = 'slide.crm_custom_reaction__v', HEADER_SLIDE_DISABLE_ACTIONS = 'slide.crm_disable_actions__v', HEADER_SLIDE_CRM_TARGET = 'slide.crm_target_platform__v', HEADER_SLIDE_CRM_ORG = 'slide.crm_org__v.crm_org_id__v', HEADER_SLIDE_CRM_PRODUCT = 'slide.crm_product__v.id', // Get the Veeva ID from the URL
34
57
  HEADER_SLIDE_CRM_CONTENT_GROUP = 'slide.crm_content_group__v.id', // Get the Veeva ID from the URL
35
58
  HEADER_SLIDE_CRM_DETAIL_GROUP = 'slide.crm_detail_group__v.id', // Get the Veeva ID from the URL
36
- HEADER_SLIDE_CRM_SHARED = 'slide.crm_shared_resource__v', HEADER_SLIDE_DETAIL_GROUP = 'slide.detail_group__v';
59
+ HEADER_SLIDE_CRM_SHARED = 'slide.crm_shared_resource__v', HEADER_SLIDE_DETAIL_GROUP = 'slide.detail_group__v', HEADER_SLIDE_EXP_DATE = 'slide.expiration_date__v';
37
60
  const CSV_HEADERS = [
38
61
  HEADER_EXTERNAL_ID,
39
62
  HEADER_TYPE,
@@ -71,7 +94,8 @@ const BINDER_PRES_HEADERS = {
71
94
  crmProduct: HEADER_PRES_CRM_PRODUCT,
72
95
  crmContentGroup: HEADER_PRES_CRM_CONTENT_GROUP,
73
96
  crmDetailGroup: HEADER_PRES_CRM_DETAIL_GROUP,
74
- detailGroup: HEADER_PRES_DETAIL_GROUP
97
+ detailGroup: HEADER_PRES_DETAIL_GROUP,
98
+ expirationDate: HEADER_PRES_EXP_DATE
75
99
  };
76
100
  const BINDER_SLIDE_HEADERS = {
77
101
  language: HEADER_SLIDE_LANGUAGE,
@@ -80,9 +104,10 @@ const BINDER_SLIDE_HEADERS = {
80
104
  crmProduct: HEADER_SLIDE_CRM_PRODUCT,
81
105
  crmContentGroup: HEADER_SLIDE_CRM_CONTENT_GROUP,
82
106
  crmDetailGroup: HEADER_SLIDE_CRM_DETAIL_GROUP,
83
- detailGroup: HEADER_SLIDE_DETAIL_GROUP
107
+ detailGroup: HEADER_SLIDE_DETAIL_GROUP,
108
+ expirationDate: HEADER_SLIDE_EXP_DATE
84
109
  };
85
- function createPresentationRow(headers, config) {
110
+ function createPresentationRow(headers, config, additional) {
86
111
  const row = new Array(headers.length).fill('');
87
112
  const presentation = config.presentation;
88
113
  addColumn(headers, row, HEADER_FIELDS_ONLY, VEEVA_YES);
@@ -101,9 +126,12 @@ function createPresentationRow(headers, config) {
101
126
  for (const [key, header] of Object.entries(BINDER_PRES_HEADERS)) {
102
127
  addColumn(headers, row, header, presentation[key]);
103
128
  }
129
+ additional?.forEach((a) => {
130
+ addColumn(headers, row, a.header, a.property ? presentation[a.property] : a.value);
131
+ });
104
132
  return row;
105
133
  }
106
- function createSlideRow(headers, config, slide) {
134
+ function createSlideRow(headers, config, slide, additional) {
107
135
  const row = new Array(headers.length).fill('');
108
136
  addColumn(headers, row, HEADER_FIELDS_ONLY, VEEVA_YES);
109
137
  addColumn(headers, row, HEADER_EXTERNAL_ID, slide.externalId);
@@ -133,9 +161,12 @@ function createSlideRow(headers, config, slide) {
133
161
  for (const [key, header] of Object.entries(BINDER_SLIDE_HEADERS)) {
134
162
  addColumn(headers, row, header, slide[key] ?? config.presentation.slideSettings?.[key] ?? config.slideSettings?.[key]);
135
163
  }
164
+ additional?.forEach((a) => {
165
+ addColumn(headers, row, a.header, a.property ? slide[a.property] : a.value);
166
+ });
136
167
  return row;
137
168
  }
138
- function createSharedRow(headers, config) {
169
+ function createSharedRow(headers, config, additional) {
139
170
  const shared = config.presentation.shared;
140
171
  if (!shared) {
141
172
  return null;
@@ -157,6 +188,9 @@ function createSharedRow(headers, config) {
157
188
  for (const [key, header] of Object.entries(BINDER_SLIDE_HEADERS)) {
158
189
  addColumn(headers, row, header, shared[key] ?? config.presentation.slideSettings?.[key] ?? config.slideSettings?.[key]);
159
190
  }
191
+ additional?.forEach((a) => {
192
+ addColumn(headers, row, a.header, a.property ? shared[a.property] : a.value);
193
+ });
160
194
  return row;
161
195
  }
162
196
  function addColumn(headers, row, header, value) {
@@ -21,6 +21,7 @@ export interface IVeevaClmBinderFields {
21
21
  crmContentGroup?: VeevaIdField | VeevaIdField[];
22
22
  crmDetailGroup?: VeevaIdField;
23
23
  detailGroup?: string;
24
+ expirationDate?: `${string}/${string}/${string}`;
24
25
  }
25
26
 
26
27
 
package/package.json CHANGED
@@ -1,19 +1,27 @@
1
1
  {
2
2
  "name": "@inizioevoke/veeva-astroclm-core",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",
7
7
  "type": "module",
8
8
  "scripts": {
9
9
  "prepublishOnly": "npm run build",
10
- "build": "npx tsx build.ts --cmd pre && tsc && npx tsx build.ts --cmd post"
10
+ "build": "npx tsx build.ts --cmd pre && tsc && npx tsx build.ts --cmd post",
11
+ "postinstall": "node postinstall.mjs"
11
12
  },
12
13
  "exports": {
13
14
  "./apps": "./dist/apps/index.js",
14
15
  "./env": "./dist/env/index.js",
15
16
  "./integrations": "./dist/integrations/index.js",
16
- "./lib": "./dist/lib/index.js",
17
+ "./lib": {
18
+ "types": "./dist/lib/index.d.ts",
19
+ "default": "./dist/lib/index.js"
20
+ },
21
+ "./lib/server": {
22
+ "types": "./dist/lib/server.d.ts",
23
+ "default": "./dist/lib/server.js"
24
+ },
17
25
  "./tasks": "./dist/tasks/index.js",
18
26
  "./types": "./dist/types/index.d.ts"
19
27
  },
@@ -0,0 +1,17 @@
1
+ import { copyFileSync, mkdirSync, rmSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ // Resolve the consuming project's root (three levels up from node_modules/@inizioevoke/astro-core)
8
+ const projectRoot = join(__dirname, '..', '..', '..');
9
+ const destDir = join(projectRoot, '.claude', 'skills', 'inizioevoke-veeva-astroclm-core');
10
+ const src = join(__dirname, 'agents', 'claude', 'SKILL.md');
11
+ const dest = join(destDir, 'SKILL.md');
12
+
13
+ rmSync(destDir, { recursive: true, force: true });
14
+ mkdirSync(destDir, { recursive: true });
15
+
16
+ copyFileSync(src, dest);
17
+ console.log(`[@inizioevoke/veeva-astroclm-core] Copied skill to ${dest}`);
package/src/lib/index.ts CHANGED
@@ -1,5 +1,2 @@
1
- export * from './const.js';
2
- export { default as loadEnv } from './env.js';
3
- export { default as parseArgv } from './parse-argv.js';
4
- export { default as parseEnv } from './parse-env.js';
1
+ // only export client-side compatible scripts
5
2
  export * from './utils.js';
@@ -0,0 +1,5 @@
1
+ export * from './const.js';
2
+ export { default as loadEnv } from './env.js';
3
+ export { default as parseArgv } from './parse-argv.js';
4
+ export { default as parseEnv } from './parse-env.js';
5
+ export * from './utils.js';
@@ -4,21 +4,48 @@ import type { IVeevaConfig, IVeevaConfigSlide, Actions, IVeevaClmBinderFields }
4
4
  import logger from '../lib/logger.js';
5
5
  import { pathToSlideZip } from '../lib/utils.js';
6
6
 
7
+ export interface AdditionalField {
8
+ header: string;
9
+ value?: string;
10
+ property?: string;
11
+ }
12
+
7
13
  interface Params {
8
14
  config: IVeevaConfig
9
15
  deployDir: string;
10
16
  buildName: string;
11
17
  fieldsOnly?: boolean;
12
18
  additional?: {
13
- headers: string[];
14
- presentation: string[];
15
- slides: string[][];
16
- shared: string[];
19
+ presentation: AdditionalField[];
20
+ slides: AdditionalField[];
21
+ shared: AdditionalField[];
17
22
  }
18
23
  }
19
24
  export default async function({ config, deployDir, buildName, fieldsOnly = false, additional }: Params) {
20
25
  logger.info('Creating multichannel loader CSV');
21
- const headers: string[] = (fieldsOnly ? CSV_HEADERS_FIELDS_ONLY : CSV_HEADERS).concat(additional?.headers ?? []);
26
+ const headers: string[] = fieldsOnly ? CSV_HEADERS_FIELDS_ONLY : CSV_HEADERS;
27
+ additional?.presentation?.forEach((a) => {
28
+ if (a.value || (a.property && config.presentation[a.property as keyof typeof config.presentation])) {
29
+ headers.push(a.header);
30
+ }
31
+ });
32
+ additional?.slides?.forEach((a) => {
33
+ if (a.value) {
34
+ headers.push(a.header);
35
+ } else if (a.property && config.presentation.slides) {
36
+ const slide = config.presentation.slides.find((s) => {
37
+ return s[a.property as keyof typeof s]
38
+ });
39
+ if (slide) {
40
+ headers.push(a.header);
41
+ }
42
+ }
43
+ });
44
+ additional?.shared?.forEach((a) => {
45
+ if (a.value || (a.property && config.presentation.shared && config.presentation.shared[a.property as keyof typeof config.presentation.shared])) {
46
+ headers.push(a.header);
47
+ }
48
+ });
22
49
  const presentation = config.presentation;
23
50
 
24
51
  for (const [key, header] of Object.entries(BINDER_PRES_HEADERS)) {
@@ -33,12 +60,12 @@ export default async function({ config, deployDir, buildName, fieldsOnly = false
33
60
  }
34
61
 
35
62
  const rows: string[][] = [headers];
36
- rows.push(createPresentationRow(headers, config).concat(additional?.presentation ?? []));
63
+ rows.push(createPresentationRow(headers, config, additional?.presentation));
37
64
  for (let i=0; i<config.presentation.slides.length; i++) {
38
- rows.push(createSlideRow(headers, config, config.presentation.slides[i]).concat(additional?.slides?.[i] ?? []));
65
+ rows.push(createSlideRow(headers, config, config.presentation.slides[i], additional?.slides));
39
66
  }
40
- const shared = createSharedRow(headers, config);
41
- shared && rows.push(shared.concat(additional?.shared ?? []));
67
+ const shared = createSharedRow(headers, config, additional?.shared);
68
+ shared && rows.push(shared);
42
69
 
43
70
  await writeFile(join(deployDir, `_${config.presentation.presentationId}_${buildName}.csv`), rows.map((r)=>{return r.join(',');}).join('\n'), { encoding: 'utf8' });
44
71
  }
@@ -76,6 +103,7 @@ const
76
103
  HEADER_PRES_CRM_CONTENT_GROUP = 'pres.crm_content_group__v.id', // Get the Veeva ID from the URL
77
104
  HEADER_PRES_CRM_DETAIL_GROUP = 'pres.crm_detail_group__v.id', // Get the Veeva ID from the URL
78
105
  HEADER_PRES_DETAIL_GROUP = 'pres.detail_group__v',
106
+ HEADER_PRES_EXP_DATE = 'pres.expiration_date__v',
79
107
 
80
108
  HEADER_PRESENTATION = 'Presentation Link',
81
109
  HEADER_SLIDE_TITLE = 'slide.title__v',
@@ -95,7 +123,8 @@ const
95
123
  HEADER_SLIDE_CRM_CONTENT_GROUP = 'slide.crm_content_group__v.id', // Get the Veeva ID from the URL
96
124
  HEADER_SLIDE_CRM_DETAIL_GROUP = 'slide.crm_detail_group__v.id', // Get the Veeva ID from the URL
97
125
  HEADER_SLIDE_CRM_SHARED = 'slide.crm_shared_resource__v',
98
- HEADER_SLIDE_DETAIL_GROUP = 'slide.detail_group__v';
126
+ HEADER_SLIDE_DETAIL_GROUP = 'slide.detail_group__v',
127
+ HEADER_SLIDE_EXP_DATE = 'slide.expiration_date__v';
99
128
 
100
129
  const CSV_HEADERS = [
101
130
  HEADER_EXTERNAL_ID,
@@ -136,7 +165,8 @@ const BINDER_PRES_HEADERS: Record<keyof IVeevaClmBinderFields, string> = {
136
165
  crmProduct: HEADER_PRES_CRM_PRODUCT,
137
166
  crmContentGroup: HEADER_PRES_CRM_CONTENT_GROUP,
138
167
  crmDetailGroup: HEADER_PRES_CRM_DETAIL_GROUP,
139
- detailGroup: HEADER_PRES_DETAIL_GROUP
168
+ detailGroup: HEADER_PRES_DETAIL_GROUP,
169
+ expirationDate: HEADER_PRES_EXP_DATE
140
170
  };
141
171
  const BINDER_SLIDE_HEADERS: Record<keyof IVeevaClmBinderFields, string> = {
142
172
  language: HEADER_SLIDE_LANGUAGE,
@@ -145,11 +175,12 @@ const BINDER_SLIDE_HEADERS: Record<keyof IVeevaClmBinderFields, string> = {
145
175
  crmProduct: HEADER_SLIDE_CRM_PRODUCT,
146
176
  crmContentGroup: HEADER_SLIDE_CRM_CONTENT_GROUP,
147
177
  crmDetailGroup: HEADER_SLIDE_CRM_DETAIL_GROUP,
148
- detailGroup: HEADER_SLIDE_DETAIL_GROUP
178
+ detailGroup: HEADER_SLIDE_DETAIL_GROUP,
179
+ expirationDate: HEADER_SLIDE_EXP_DATE
149
180
  };
150
181
 
151
182
 
152
- function createPresentationRow(headers: string[], config: IVeevaConfig): string[] {
183
+ function createPresentationRow(headers: string[], config: IVeevaConfig, additional?: AdditionalField[]): string[] {
153
184
  const row: string[] = new Array(headers.length).fill('');
154
185
  const presentation = config.presentation;
155
186
  addColumn(headers, row, HEADER_FIELDS_ONLY, VEEVA_YES);
@@ -168,10 +199,13 @@ function createPresentationRow(headers: string[], config: IVeevaConfig): string[
168
199
  for (const [key, header] of Object.entries(BINDER_PRES_HEADERS)) {
169
200
  addColumn(headers, row, header, presentation[key as keyof IVeevaClmBinderFields]);
170
201
  }
202
+ additional?.forEach((a) => {
203
+ addColumn(headers, row, a.header, a.property ? presentation[a.property as keyof typeof presentation] : a.value)
204
+ });
171
205
  return row;
172
206
  }
173
207
 
174
- function createSlideRow(headers: string[], config: IVeevaConfig, slide: IVeevaConfigSlide): string[] {
208
+ function createSlideRow(headers: string[], config: IVeevaConfig, slide: IVeevaConfigSlide, additional?: AdditionalField[]): string[] {
175
209
  const row: string[] = new Array(headers.length).fill('');
176
210
  addColumn(headers, row, HEADER_FIELDS_ONLY, VEEVA_YES);
177
211
  addColumn(headers, row, HEADER_EXTERNAL_ID, slide.externalId);
@@ -206,10 +240,13 @@ function createSlideRow(headers: string[], config: IVeevaConfig, slide: IVeevaCo
206
240
  for (const [key, header] of Object.entries(BINDER_SLIDE_HEADERS)) {
207
241
  addColumn(headers, row, header, slide[key as keyof IVeevaClmBinderFields] ?? config.presentation.slideSettings?.[key as keyof IVeevaClmBinderFields] ?? config.slideSettings?.[key as keyof IVeevaClmBinderFields]);
208
242
  }
243
+ additional?.forEach((a) => {
244
+ addColumn(headers, row, a.header, a.property ? slide[a.property as keyof typeof slide] : a.value)
245
+ });
209
246
  return row;
210
247
  }
211
248
 
212
- function createSharedRow(headers: string[], config: IVeevaConfig): string[] | null {
249
+ function createSharedRow(headers: string[], config: IVeevaConfig, additional?: AdditionalField[]): string[] | null {
213
250
  const shared = config.presentation.shared;
214
251
  if (!shared) {
215
252
  return null;
@@ -231,6 +268,9 @@ function createSharedRow(headers: string[], config: IVeevaConfig): string[] | nu
231
268
  for (const [key, header] of Object.entries(BINDER_SLIDE_HEADERS)) {
232
269
  addColumn(headers, row, header, shared[key as keyof IVeevaClmBinderFields] ?? config.presentation.slideSettings?.[key as keyof IVeevaClmBinderFields] ?? config.slideSettings?.[key as keyof IVeevaClmBinderFields]);
233
270
  }
271
+ additional?.forEach((a) => {
272
+ addColumn(headers, row, a.header, a.property ? shared[a.property as keyof typeof shared] : a.value)
273
+ });
234
274
  return row;
235
275
  }
236
276
 
@@ -21,6 +21,7 @@ export interface IVeevaClmBinderFields {
21
21
  crmContentGroup?: VeevaIdField | VeevaIdField[];
22
22
  crmDetailGroup?: VeevaIdField;
23
23
  detailGroup?: string;
24
+ expirationDate?: `${string}/${string}/${string}`;
24
25
  }
25
26
 
26
27