@servicenow/sdk-build-core 4.7.2 → 4.8.1

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.
Files changed (79) hide show
  1. package/dist/compiler.d.ts +7 -0
  2. package/dist/compiler.js +22 -6
  3. package/dist/compiler.js.map +1 -1
  4. package/dist/compression.d.ts +1 -1
  5. package/dist/compression.js +28 -48
  6. package/dist/compression.js.map +1 -1
  7. package/dist/diagnostic.js +17 -7
  8. package/dist/diagnostic.js.map +1 -1
  9. package/dist/formatter.js +17 -7
  10. package/dist/formatter.js.map +1 -1
  11. package/dist/fs.js +17 -7
  12. package/dist/fs.js.map +1 -1
  13. package/dist/glob.d.ts +4 -0
  14. package/dist/glob.js +23 -0
  15. package/dist/glob.js.map +1 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/json.js +17 -7
  20. package/dist/json.js.map +1 -1
  21. package/dist/keys-registry.js +17 -7
  22. package/dist/keys-registry.js.map +1 -1
  23. package/dist/now-config-dependencies.js +17 -7
  24. package/dist/now-config-dependencies.js.map +1 -1
  25. package/dist/now-config.d.ts +10 -0
  26. package/dist/now-config.js +19 -7
  27. package/dist/now-config.js.map +1 -1
  28. package/dist/package-inventory.js.map +1 -1
  29. package/dist/plugins/plugin.d.ts +4 -0
  30. package/dist/plugins/plugin.js +17 -1
  31. package/dist/plugins/plugin.js.map +1 -1
  32. package/dist/plugins/post-install.d.ts +7 -0
  33. package/dist/plugins/shape.d.ts +13 -0
  34. package/dist/plugins/shape.js +43 -7
  35. package/dist/plugins/shape.js.map +1 -1
  36. package/dist/plugins/time.d.ts +5 -0
  37. package/dist/plugins/time.js +8 -0
  38. package/dist/plugins/time.js.map +1 -1
  39. package/dist/taxonomy.d.ts +1169 -6
  40. package/dist/taxonomy.js +35 -8
  41. package/dist/taxonomy.js.map +1 -1
  42. package/dist/telemetry/clients/abstract-client.d.ts +6 -0
  43. package/dist/telemetry/clients/abstract-client.js +13 -4
  44. package/dist/telemetry/clients/abstract-client.js.map +1 -1
  45. package/dist/telemetry/clients/browser-client.d.ts +2 -0
  46. package/dist/telemetry/clients/browser-client.js +1 -0
  47. package/dist/telemetry/clients/browser-client.js.map +1 -1
  48. package/dist/telemetry/clients/node-client.d.ts +1 -0
  49. package/dist/telemetry/clients/node-client.js +25 -10
  50. package/dist/telemetry/clients/node-client.js.map +1 -1
  51. package/dist/telemetry/factory.d.ts +1 -0
  52. package/dist/telemetry/factory.js.map +1 -1
  53. package/dist/typescript.js +17 -7
  54. package/dist/typescript.js.map +1 -1
  55. package/dist/util/conditional-dir.d.ts +13 -0
  56. package/dist/util/conditional-dir.js +22 -0
  57. package/dist/util/conditional-dir.js.map +1 -0
  58. package/dist/util/index.d.ts +1 -0
  59. package/dist/util/index.js +1 -0
  60. package/dist/util/index.js.map +1 -1
  61. package/now.config.schema.json +15 -1
  62. package/package.json +3 -3
  63. package/src/compiler.ts +25 -6
  64. package/src/compression.ts +34 -53
  65. package/src/glob.ts +20 -0
  66. package/src/index.ts +1 -0
  67. package/src/now-config.ts +4 -0
  68. package/src/package-inventory.ts +1 -1
  69. package/src/plugins/plugin.ts +19 -2
  70. package/src/plugins/post-install.ts +8 -0
  71. package/src/plugins/shape.ts +28 -0
  72. package/src/plugins/time.ts +8 -0
  73. package/src/taxonomy.ts +18 -1
  74. package/src/telemetry/clients/abstract-client.ts +15 -4
  75. package/src/telemetry/clients/browser-client.ts +2 -0
  76. package/src/telemetry/clients/node-client.ts +13 -4
  77. package/src/telemetry/factory.ts +7 -1
  78. package/src/util/conditional-dir.ts +19 -0
  79. package/src/util/index.ts +1 -0
package/src/compiler.ts CHANGED
@@ -342,13 +342,17 @@ declare module '*.html' {
342
342
  return this.generatedTableFilePath === sourcePath
343
343
  }
344
344
 
345
- // Cache for table column types that need coercion (boolean/number only).
346
- // Populated on first call to getTableColumnTypes(); subsequent calls return immediately.
345
+ // Caches populated by resolveDataTypeProperties() on first access per table.
347
346
  private tableColumnTypesCache = new Map<string, Map<string, 'boolean' | 'number' | 'array' | 'array-optional'>>()
347
+ private mandatoryColumnsCache = new Map<string, Set<string>>()
348
348
 
349
- getTableColumnTypes(tableName: string): Map<string, 'boolean' | 'number' | 'array' | 'array-optional'> | undefined {
349
+ /**
350
+ * Resolves Data<T> for the given table and populates both the column-type
351
+ * coercion cache and the mandatory-columns cache in a single pass.
352
+ */
353
+ private resolveDataTypeProperties(tableName: string): void {
350
354
  if (this.tableColumnTypesCache.has(tableName)) {
351
- return this.tableColumnTypesCache.get(tableName)
355
+ return
352
356
  }
353
357
 
354
358
  const tempFilePath = path.join(this.rootDir, 'src', '$$TEMP_DATA_RESOLVER$$.ts')
@@ -362,14 +366,19 @@ declare module '*.html' {
362
366
  try {
363
367
  const typeAlias = tempSource.getTypeAlias('T')
364
368
  if (!typeAlias) {
365
- return undefined
369
+ return
366
370
  }
367
371
 
368
372
  const resolvedType = typeAlias.getType()
369
373
  const properties = resolvedType.getProperties()
370
374
  const columnTypes = new Map<string, 'boolean' | 'number' | 'array' | 'array-optional'>()
375
+ const mandatoryColumns = new Set<string>()
371
376
 
372
377
  properties.forEach((property) => {
378
+ if (!property.isOptional()) {
379
+ mandatoryColumns.add(property.getName())
380
+ }
381
+
373
382
  const propType = property.getTypeAtLocation(typeAlias)
374
383
  const typeText = propType.getText()
375
384
 
@@ -383,12 +392,22 @@ declare module '*.html' {
383
392
  })
384
393
 
385
394
  this.tableColumnTypesCache.set(tableName, columnTypes)
386
- return columnTypes
395
+ this.mandatoryColumnsCache.set(tableName, mandatoryColumns)
387
396
  } finally {
388
397
  this.removeSourceFile(tempSource)
389
398
  }
390
399
  }
391
400
 
401
+ getTableColumnTypes(tableName: string): Map<string, 'boolean' | 'number' | 'array' | 'array-optional'> | undefined {
402
+ this.resolveDataTypeProperties(tableName)
403
+ return this.tableColumnTypesCache.get(tableName)
404
+ }
405
+
406
+ getMandatoryColumns(tableName: string): Set<string> | undefined {
407
+ this.resolveDataTypeProperties(tableName)
408
+ return this.mandatoryColumnsCache.get(tableName)
409
+ }
410
+
392
411
  private createModuleProject(fs: FileSystem, tsConfigPath?: string) {
393
412
  return new ts.Project({
394
413
  fileSystem: new TsMorphFileSystemWrapper(fs),
@@ -1,4 +1,5 @@
1
- import { ZipDeflate, unzipSync, zipSync, gunzip as callbackGunzip, Zip, gzipSync, gunzipSync } from 'fflate'
1
+ import { unzipSync, zipSync, gunzip as callbackGunzip, gzipSync, gunzipSync } from 'fflate'
2
+ import type { Zippable } from 'fflate'
2
3
  import { FileSystem, TsMorphFileSystemWrapper } from './fs'
3
4
  import type { Logger } from './logger'
4
5
  import { path } from './path'
@@ -9,7 +10,8 @@ export const unzipAppPackage = async (
9
10
  fs: FileSystem,
10
11
  zipPath: string,
11
12
  targetPath: string,
12
- logger?: Logger
13
+ logger?: Logger,
14
+ ignore?: (filename: string) => boolean
13
15
  ): Promise<{ files: string[] }> => {
14
16
  const zipArrBuf = new Uint8Array(fs.readFileSync(zipPath))
15
17
  const unzipResult = unzipSync(zipArrBuf)
@@ -31,72 +33,51 @@ export const unzipAppPackage = async (
31
33
  delete unzipResult[ZIP_STATUS_FILE]
32
34
  }
33
35
 
34
- const files = Object.entries(unzipResult).map(([filePath, data]) => {
36
+ const files: string[] = []
37
+ for (const [filePath, data] of Object.entries(unzipResult)) {
38
+ const filename = path.basename(filePath)
39
+
40
+ if (ignore?.(filename)) {
41
+ logger?.info(`Skipping file ${filename} due to excludeFilePatterns`)
42
+ continue
43
+ }
44
+
35
45
  const fullPath = path.join(targetPath, filePath)
36
46
 
37
47
  // Create directories if necessary
38
- const dir = path.dirname(fullPath)
39
- FileSystem.ensureDirSync(fs, dir)
48
+ FileSystem.ensureDirSync(fs, path.dirname(fullPath))
40
49
 
41
50
  // Write file content to the filesystem
42
51
  fs.writeFileSync(fullPath, data)
43
52
 
44
- return fullPath
45
- })
53
+ files.push(fullPath)
54
+ }
46
55
 
47
56
  return { files }
48
57
  }
49
58
 
50
59
  export const zipDirectory = async (fs: FileSystem, source: string, destination: string): Promise<string> => {
51
- return new Promise<string>((resolve, reject) => {
52
- try {
53
- const z = new Zip()
54
-
55
- const outputStream = FileSystem.createWriteStream(fs, destination)
56
-
57
- z.ondata = (err, data, final) => {
58
- if (err) {
59
- outputStream.destroy()
60
- reject(err)
61
- } else {
62
- outputStream.write(data)
63
- if (final) {
64
- outputStream.end()
65
- }
66
- }
67
- }
68
-
69
- outputStream.on('finish', () => {
70
- resolve(destination)
71
- })
72
-
73
- outputStream.on('error', (error) => {
74
- reject(error)
75
- })
76
-
77
- // Using our own glob for browser compatibility
78
- const wrappedFs = new TsMorphFileSystemWrapper(fs)
79
- const files = wrappedFs.globSync(['**'], {
80
- cwd: source,
81
- })
60
+ // Using our own glob for browser compatibility
61
+ const wrappedFs = new TsMorphFileSystemWrapper(fs)
62
+ const files = wrappedFs.globSync(['**'], {
63
+ cwd: source,
64
+ })
82
65
 
83
- if (files.length === 0) {
84
- reject(`No files found in ${source} directory to create zip.`)
85
- }
66
+ if (files.length === 0) {
67
+ throw new Error(`No files found in ${source} directory to create zip.`)
68
+ }
86
69
 
87
- for (const file of files) {
88
- const fp = path.normalize(file)
89
- const data = fs.readFileSync(fp)
90
- const zf = new ZipDeflate(fp.replace(source, ''), { level: 6 })
91
- z.add(zf)
92
- zf.push(data, true)
93
- }
70
+ const fileMap: Zippable = {}
71
+ for (const file of files) {
72
+ const fp = path.normalize(file)
73
+ const data = new Uint8Array(fs.readFileSync(fp))
74
+ const entryName = fp.replace(source, '')
75
+ fileMap[entryName] = [data, { level: 6 }]
76
+ }
94
77
 
95
- z.end()
96
- } catch (err) {
97
- reject(err)
98
- }
99
- })
78
+ const zipped = zipSync(fileMap)
79
+ fs.writeFileSync(destination, zipped)
80
+ return destination
100
81
  }
101
82
 
102
83
  const gunzip = (data: Uint8Array) =>
package/src/glob.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Converts a glob pattern to a RegExp.
3
+ * Supports `*` (match within a path segment), `**` (match across segments), and `?` (single character).
4
+ */
5
+ function globToRegex(pattern: string): RegExp {
6
+ const escaped = pattern
7
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars (not * or ?)
8
+ .replace(/\*\*/g, '\0') // placeholder for **
9
+ .replace(/\*/g, '[^/]*') // * → match within segment
10
+ .replace(/\0/g, '.*') // ** → match across segments
11
+ .replace(/\?/g, '[^/]') // ? → single char within segment
12
+ return new RegExp(`^${escaped}$`)
13
+ }
14
+
15
+ /**
16
+ * Returns true if `str` matches any of the provided glob patterns.
17
+ */
18
+ export function matchesGlob(str: string, patterns: string[]): boolean {
19
+ return patterns.some((pattern) => globToRegex(pattern).test(str))
20
+ }
package/src/index.ts CHANGED
@@ -22,3 +22,4 @@ export * from './formatter'
22
22
  export * from './telemetry'
23
23
  export * from './claims'
24
24
  export * from './taxonomy'
25
+ export * from './glob'
package/src/now-config.ts CHANGED
@@ -254,6 +254,7 @@ const NowConfigSchema = z
254
254
  npmUpdateCheck: z.literal(false).or(z.number()).default(10),
255
255
  dependencies: dependenciesSchema,
256
256
  ignoreTransformTableList: z.array(z.string()).default([]),
257
+ excludeFilePatterns: z.array(z.string()).default([]),
257
258
  trustedModules: z.array(z.string()).default([]),
258
259
  tsconfigPath: z.string().optional(),
259
260
  scripts: z.record(z.string(), z.string()).default({}),
@@ -309,6 +310,7 @@ const NowConfigSchema = z
309
310
  type: z.enum(['configuration', 'package']).default('package'),
310
311
  taxonomy,
311
312
  hostedPlugins: z.record(z.string(), z.string()).optional(),
313
+ sysCode: z.string().optional(),
312
314
 
313
315
  // Application Runtime Policy (ARP) fields
314
316
  applicationRuntimePolicy: z.enum(['none', 'tracking', 'enforcing']).default('none'),
@@ -329,6 +331,7 @@ const NowConfigSchema = z
329
331
  | 'npmUpdateCheck'
330
332
  | 'dependencies'
331
333
  | 'ignoreTransformTableList'
334
+ | 'excludeFilePatterns'
332
335
  | 'trustedModules'
333
336
  | 'modulePaths'
334
337
  | 'serverModulesIncludePatterns'
@@ -354,6 +357,7 @@ const NowConfigSchema = z
354
357
  | 'type'
355
358
  | 'taxonomy'
356
359
  | 'hostedPlugins'
360
+ | 'sysCode'
357
361
  | 'applicationRuntimePolicy'
358
362
  | 'networkPolicies'
359
363
  | 'wildcardPolicy'
@@ -14,7 +14,7 @@ export interface PackageInventoryConfig {
14
14
  */
15
15
  export async function sha256(content: string | Uint8Array): Promise<string> {
16
16
  const data = typeof content === 'string' ? new TextEncoder().encode(content) : content
17
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
17
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data as BufferSource)
18
18
  const hashArray = Array.from(new Uint8Array(hashBuffer))
19
19
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
20
20
  }
@@ -19,7 +19,7 @@ import type { File, OutputFile } from './file'
19
19
  import type { Product } from './product'
20
20
  import { Database, DiffDatabase } from './database'
21
21
  import type { Context as BaseContext, Diagnostics } from './context'
22
- import { getFileType, isSNScope } from '../util'
22
+ import { getConditionalPluginDir, getFileType, isSNScope } from '../util'
23
23
  import { NOW_FILE_EXTENSION, NowConfig } from '..'
24
24
  import { path } from '@servicenow/sdk-build-core'
25
25
 
@@ -839,6 +839,23 @@ export class Plugin {
839
839
  return this.traverseDescendants(parent, database)
840
840
  }
841
841
 
842
+ /**
843
+ * Determines whether the conditional directory of a (if/hosted) child matches that of it's parent.
844
+ */
845
+ private isMixedConditionalChild(parent: Record, child: Record): boolean {
846
+ const parentPlugin = getConditionalPluginDir(parent.getOriginalFilePath())
847
+ const childPlugin = getConditionalPluginDir(child.getOriginalFilePath())
848
+ if (!parentPlugin && !childPlugin) {
849
+ return false
850
+ } else if (!parentPlugin || !childPlugin) {
851
+ return true
852
+ }
853
+
854
+ // both conditional - the child belongs to this parent only if they
855
+ // target the same conditional plugin (the if/<plugin> or hosted_plugins/<plugin> segment)
856
+ return parentPlugin !== childPlugin
857
+ }
858
+
842
859
  private traverseDescendants(parent: Record, database: Database, visited: Set<string> = new Set()): Record[] {
843
860
  visited.add(parent.getId().getValue())
844
861
 
@@ -848,7 +865,7 @@ export class Plugin {
848
865
  const descendants: Record[] = []
849
866
  const push = (children: Record | Record[] | undefined) => {
850
867
  for (const child of [children].flat()) {
851
- if (child && !visited.has(child.getId().getValue())) {
868
+ if (child && !visited.has(child.getId().getValue()) && !this.isMixedConditionalChild(parent, child)) {
852
869
  visited.add(child.getId().getValue())
853
870
  descendants.push(child)
854
871
  descendants.push(...this.traverseDescendants(child, database, visited))
@@ -27,8 +27,16 @@ export type InstanceClient = {
27
27
  fetch(path: string, init?: RequestInit, params?: URLSearchParams): Promise<Response>
28
28
  }
29
29
 
30
+ export type RecordEntry = {
31
+ sys_id: string
32
+ active: string
33
+ state: string
34
+ }
35
+
30
36
  export type PostInstallContext = {
31
37
  instanceClient?: InstanceClient
32
38
  logger: Logger
33
39
  config: { scopeId: string; scope: string; [key: string]: unknown }
40
+ /** Map of table name → array of record entries from the project's keys registry */
41
+ recordIds?: Record<string, RecordEntry[]>
34
42
  }
@@ -658,6 +658,20 @@ export class TemplateSpanShape extends StringShape {
658
658
  return `${substitution instanceof Shape ? substitution.getValue() : typeof substitution === 'function' ? substitution(this.expression) : (substitution ?? this.expression.toString().getValue())}${this.literalText}`
659
659
  }
660
660
 
661
+ /**
662
+ * Override equals to compare literal text and expression code instead of
663
+ * rendered values. The base getValue() resolves expressions via
664
+ * toString().getValue() which produces identical strings for structurally
665
+ * different CallExpressionShape instances, causing the commit system to
666
+ * incorrectly skip updates.
667
+ */
668
+ override equals(other: unknown): boolean {
669
+ if (other instanceof TemplateSpanShape) {
670
+ return this.literalText === other.literalText && this.expression.getCode() === other.expression.getCode()
671
+ }
672
+ return super.equals(other)
673
+ }
674
+
661
675
  override getCode(): string {
662
676
  return `$\{${this.expression.getCode()}}${StringShape.escapeBackticks(this.literalText)}`
663
677
  }
@@ -697,6 +711,20 @@ export class TemplateExpressionShape extends StringShape {
697
711
  return `${this.literalText}${this.spans.map((s, i) => s.getValue(typeof substitutions === 'function' ? substitutions : substitutions?.[i])).join('')}`
698
712
  }
699
713
 
714
+ /**
715
+ * Override equals to compare literal text and spans structurally instead of
716
+ * using rendered getValue() strings. See TemplateSpanShape.equals() for details.
717
+ */
718
+ override equals(other: unknown): boolean {
719
+ if (other instanceof TemplateExpressionShape) {
720
+ if (this.literalText !== other.literalText || this.spans.length !== other.spans.length) {
721
+ return false
722
+ }
723
+ return this.spans.every((span, i) => span.equals(other.spans[i]))
724
+ }
725
+ return super.equals(other)
726
+ }
727
+
700
728
  override getCode(): string {
701
729
  return `\`${StringShape.escapeBackticks(this.literalText)}${this.spans.map((s) => s.getCode()).join('')}\``
702
730
  }
@@ -175,6 +175,14 @@ export function parseDateTime(dateTimeStr: string) {
175
175
  return undefined
176
176
  }
177
177
 
178
+ /**
179
+ * Convert a Duration object to the platform's epoch-relative datetime string.
180
+ * E.g. `{ days: 4, hours: 3, minutes: 2, seconds: 1 }` → `'1970-01-05 03:02:01'`
181
+ */
182
+ export function formatDuration(duration: Duration): string {
183
+ return formatDateToPlatformFormat(durationFieldToXML(duration))
184
+ }
185
+
178
186
  /**
179
187
  * Convert a Duration object to a Date for glide_duration fields.
180
188
  * Duration fields are timezone-agnostic (they represent a span of time),
package/src/taxonomy.ts CHANGED
@@ -37,6 +37,15 @@ const TableNames = {
37
37
  SYS_FLOW_KNOWLEDGE_TRIGGER: 'sys_flow_knowledge_trigger',
38
38
  SYS_FLOW_RT_QUERY_TRIGGER: 'sys_flow_rt_query_trigger',
39
39
  SYS_FLOW_TRIGGER: 'sys_flow_trigger',
40
+ SYS_HUB_STEP_INSTANCE: 'sys_hub_step_instance',
41
+ SYS_HUB_STEP_EXT_INPUT: 'sys_hub_step_ext_input',
42
+ SYS_HUB_STEP_EXT_OUTPUT: 'sys_hub_step_ext_output',
43
+ SYS_HUB_ACTION_INPUT: 'sys_hub_action_input',
44
+ SYS_HUB_ACTION_OUTPUT: 'sys_hub_action_output',
45
+ SYS_HUB_ACTION_STATUS_METADATA: 'sys_hub_action_status_metadata',
46
+ SYS_HUB_STATUS_CONDITION: 'sys_hub_status_condition',
47
+ SYS_HUB_ACTION_TYPE_SNAPSHOT: 'sys_hub_action_type_snapshot',
48
+ SYS_HUB_ACTION_PLAN: 'sys_hub_action_plan',
40
49
 
41
50
  // Client Development tables
42
51
  DL_U_ASSIGNMENT: 'dl_u_assignment',
@@ -250,7 +259,6 @@ const taxonomyMapping = z
250
259
  [TableNames.SYS_FLOW_TRIGGER]: taxonomyItem.default('automation/trigger'),
251
260
  [TableNames.SYS_HUB_FLOW]: flowTaxonomyItem,
252
261
  [TableNames.SYS_HUB_ACTION_TYPE_DEFINITION]: flowTaxonomyItem,
253
- [TableNames.SYS_ELEMENT_MAPPING]: flowTaxonomyItem,
254
262
  [TableNames.SYS_HUB_FLOW_INPUT]: flowTaxonomyItem,
255
263
  [TableNames.SYS_HUB_FLOW_OUTPUT]: flowTaxonomyItem,
256
264
  [TableNames.SYS_HUB_ACTION_INPUT_ACTION_INSTANCE]: flowTaxonomyItem,
@@ -270,6 +278,15 @@ const taxonomyMapping = z
270
278
  [TableNames.SYS_FLOW_TRIGGER_PLAN]: flowTaxonomyItem,
271
279
  [TableNames.SYS_HUB_FLOW_SNAPSHOT]: flowTaxonomyItem,
272
280
  [TableNames.SYS_FLOW_SUBFLOW_PLAN]: flowTaxonomyItem,
281
+ [TableNames.SYS_HUB_STEP_INSTANCE]: flowTaxonomyItem,
282
+ [TableNames.SYS_HUB_STEP_EXT_INPUT]: flowTaxonomyItem,
283
+ [TableNames.SYS_HUB_STEP_EXT_OUTPUT]: flowTaxonomyItem,
284
+ [TableNames.SYS_HUB_ACTION_INPUT]: flowTaxonomyItem,
285
+ [TableNames.SYS_HUB_ACTION_OUTPUT]: flowTaxonomyItem,
286
+ [TableNames.SYS_HUB_ACTION_STATUS_METADATA]: flowTaxonomyItem,
287
+ [TableNames.SYS_HUB_STATUS_CONDITION]: flowTaxonomyItem,
288
+ [TableNames.SYS_HUB_ACTION_TYPE_SNAPSHOT]: flowTaxonomyItem,
289
+ [TableNames.SYS_HUB_ACTION_PLAN]: flowTaxonomyItem,
273
290
 
274
291
  // Client Development tables
275
292
  [TableNames.DL_U_ASSIGNMENT]: taxonomyItem.default('client-development/assignment-data-lookup'),
@@ -15,9 +15,12 @@ export abstract class AbstractAppSeeClient implements Telemetry {
15
15
  protected readonly sdkVersion?: string | undefined
16
16
  protected readonly clientName: 'cli' | 'ide' | string
17
17
  protected readonly hostname?: string | undefined
18
+ protected readonly codingAgent?: string | undefined
18
19
  protected session?: Session
19
20
  protected startPromise?: Promise<void>
20
21
  protected userId?: string
22
+ private lastEventTimestamp = 0
23
+ private configReceivedTime?: string
21
24
 
22
25
  constructor(
23
26
  protected readonly config: InstanceSettings,
@@ -25,11 +28,19 @@ export abstract class AbstractAppSeeClient implements Telemetry {
25
28
  sdkVersion?: string
26
29
  clientName: 'cli' | 'ide' | string
27
30
  hostname?: string
31
+ codingAgent?: string
28
32
  }
29
33
  ) {
30
34
  this.sdkVersion = telemetryAttributes.sdkVersion
31
35
  this.clientName = telemetryAttributes.clientName
32
36
  this.hostname = telemetryAttributes.hostname
37
+ this.codingAgent = telemetryAttributes.codingAgent
38
+ }
39
+
40
+ private nextTimestamp(): number {
41
+ const now = Date.now()
42
+ this.lastEventTimestamp = now > this.lastEventTimestamp ? now : this.lastEventTimestamp + 1
43
+ return this.lastEventTimestamp
33
44
  }
34
45
 
35
46
  startTimerEvent(name: TelemetryEvent['name']): TimerMetric {
@@ -77,19 +88,18 @@ export abstract class AbstractAppSeeClient implements Telemetry {
77
88
 
78
89
  const defaultValues = this.getDefaultDataValues()
79
90
  const eventData = Array.isArray(events) ? events : [events]
80
- const now = Date.now()
81
91
  const hostname = this.hostname ? { hostname: this.hostname } : {}
82
92
 
83
93
  const dataPoints = eventData.map<Datapoint>((d) => ({
84
94
  t: DatapointType.Event,
85
- d: now,
95
+ d: this.nextTimestamp(),
86
96
  n: d.name,
87
97
  p: { scopeId: d.appInfo?.scopeId, ...hostname, ...defaultValues, ...d.data },
88
98
  }))
89
99
 
90
100
  dataPoints.push({
91
101
  t: DatapointType.User,
92
- d: now,
102
+ d: this.nextTimestamp(),
93
103
  n: this.userId ?? 'unknown',
94
104
  p: this.getUserProps(hostname),
95
105
  })
@@ -99,7 +109,7 @@ export abstract class AbstractAppSeeClient implements Telemetry {
99
109
  DataPoints: dataPoints,
100
110
  TabId: this.session.TabId || '0',
101
111
  ClientTime: new Date().toISOString(),
102
- ConfigReceivedTime: new Date().toISOString(),
112
+ ConfigReceivedTime: this.configReceivedTime ?? new Date().toISOString(),
103
113
  }
104
114
 
105
115
  fetch(new URL('/web/heartbeat', this.config.BaseUrl), {
@@ -150,6 +160,7 @@ export abstract class AbstractAppSeeClient implements Telemetry {
150
160
  const bodyResponse = await response.json()
151
161
  if (response.ok) {
152
162
  this.session = { ...bodyResponse, TabId: tabId }
163
+ this.configReceivedTime = new Date().toISOString()
153
164
  } else {
154
165
  throw new Error(`Failed to start session with status ${response.status}: ${bodyResponse.Error}`)
155
166
  }
@@ -9,6 +9,7 @@ export class BrowserTelemetryClient extends AbstractAppSeeClient {
9
9
  clientName?: 'ide' | string
10
10
  hostname?: string
11
11
  ideVersion?: string
12
+ codingAgent?: string
12
13
  }
13
14
  ) {
14
15
  const { ideVersion, ...baseAttributes } = telemetryAttributes ?? {}
@@ -68,6 +69,7 @@ export class BrowserTelemetryClient extends AbstractAppSeeClient {
68
69
  version: this.sdkVersion || 'unknown',
69
70
  clientName: this.clientName,
70
71
  ideVersion: this.ideVersion || 'unknown',
72
+ ...(this.codingAgent ? { codingAgent: this.codingAgent } : {}),
71
73
  }
72
74
  }
73
75
 
@@ -7,7 +7,12 @@ import { detectCodingAgent, detectIde } from './detect-agent'
7
7
  export class NodeTelemetryClient extends AbstractAppSeeClient {
8
8
  constructor(
9
9
  config: InstanceSettings,
10
- telemetryAttributes?: { sdkVersion?: string; clientName?: 'cli' | string; hostname?: string }
10
+ telemetryAttributes?: {
11
+ sdkVersion?: string
12
+ clientName?: 'cli' | string
13
+ hostname?: string
14
+ codingAgent?: string
15
+ }
11
16
  ) {
12
17
  super(config, {
13
18
  ...(telemetryAttributes ?? {}),
@@ -44,9 +49,13 @@ export class NodeTelemetryClient extends AbstractAppSeeClient {
44
49
  const env = process.env
45
50
  const agentData: TelemetryEventData<{ codingAgent?: string; ide?: string }> = {}
46
51
 
47
- const codingAgent = detectCodingAgent(env)
48
- if (codingAgent) {
49
- agentData.codingAgent = codingAgent
52
+ if (this.codingAgent) {
53
+ agentData.codingAgent = this.codingAgent
54
+ } else {
55
+ const codingAgent = detectCodingAgent(env)
56
+ if (codingAgent) {
57
+ agentData.codingAgent = codingAgent
58
+ }
50
59
  }
51
60
  const ide = detectIde(env, process.platform)
52
61
  if (ide) {
@@ -8,7 +8,13 @@ import { getAppSeeConfig } from './config'
8
8
  export class TelemetryFactory {
9
9
  static create(options?: {
10
10
  type?: 'node' | 'browser'
11
- attributes?: { sdkVersion?: string; clientName?: string; hostname?: string; ideVersion?: string }
11
+ attributes?: {
12
+ sdkVersion?: string
13
+ clientName?: string
14
+ hostname?: string
15
+ ideVersion?: string
16
+ codingAgent?: string
17
+ }
12
18
  }): Telemetry {
13
19
  if (process.env['NODE_ENV'] === 'test' || !options || !options.type) {
14
20
  return new NoOpTelemetry()
@@ -0,0 +1,19 @@
1
+ const CONDITIONAL_MARKERS = ['if', 'hosted_plugins'] as const
2
+
3
+ /**
4
+ * Extracts the conditional plugin directory from a file path — the `if` or
5
+ * `hosted_plugins` marker plus the plugin segment that follows it (e.g.
6
+ * `if/com.plugin` from `.../if/com.plugin/update/x.xml`, or
7
+ * `hosted_plugins/com.plugin` from `.../hosted_plugins/com.plugin/...`).
8
+ *
9
+ * Two conditional records belong to the same app only when this value matches,
10
+ * so it is used both for grouping descendants and for detecting conditional
11
+ * conflicts. Returns undefined when the path is not under a conditional directory.
12
+ *
13
+ * Matches either path separator so it behaves the same on POSIX and Windows.
14
+ */
15
+ export function getConditionalPluginDir(filePath: string): string | undefined {
16
+ const marker = `(${CONDITIONAL_MARKERS.join('|')})`
17
+ const match = filePath.match(new RegExp(`[/\\\\]${marker}[/\\\\]([^/\\\\]+)`))
18
+ return match ? `${match[1]}/${match[2]}` : undefined
19
+ }
package/src/util/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './is-sn-scope'
2
2
  export * from './get-file-type'
3
3
  export * from './delete-multiple'
4
+ export * from './conditional-dir'