@frontera-sdk/cli 1.52.40 → 1.52.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/cli",
3
- "version": "1.52.40",
3
+ "version": "1.52.41",
4
4
  "description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/claude-agent-sdk": "^0.3.251",
42
42
  "@faker-js/faker": "10.6.0",
43
- "@frontera-sdk/core": "1.52.40",
44
- "@frontera-sdk/functions": "1.52.40",
43
+ "@frontera-sdk/core": "1.52.41",
44
+ "@frontera-sdk/functions": "1.52.41",
45
45
  "ai": "^6.0.116",
46
46
  "axe-core": "4.12.1",
47
47
  "gray-matter": "^4.0.3",
@@ -49,7 +49,7 @@
49
49
  "yaml": "^2.9.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@frontera-sdk/forge-contracts": "1.52.40",
52
+ "@frontera-sdk/forge-contracts": "1.52.41",
53
53
  "@types/bun": "^1.3.14",
54
54
  "typescript": "^5.9.3"
55
55
  }
@@ -73,7 +73,7 @@ export function ontologyEditCompatibilityFingerprint(
73
73
  ): string {
74
74
  const canonical = JSON.stringify({
75
75
  objectTypeId,
76
- usedProperties: [...new Set(usedProperties)].sort(),
76
+ usedProperties: [...new Set(usedProperties)].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)),
77
77
  })
78
78
  return `sha256:${createHash('sha256').update(canonical).digest('hex')}`
79
79
  }
@@ -79,7 +79,7 @@ export async function agentDraftChanges(api: PlatformApi, id: string): Promise<A
79
79
  (k) => stableStringify(draftSections[k]) !== stableStringify(liveSnapshot[k]),
80
80
  )
81
81
  : []),
82
- ].sort()
82
+ ].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
83
83
 
84
84
  return { hasDraft: true, published, revision: draft.revision, changed }
85
85
  }
@@ -1522,7 +1522,7 @@ export function orderForPrune(
1522
1522
  if (blocked.length > 0) {
1523
1523
  const named = blocked
1524
1524
  .map((action) => `${action.apiName} → ${action.compensates}`)
1525
- .sort()
1525
+ .sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
1526
1526
  .join(', ')
1527
1527
  throw new CliError(
1528
1528
  `${blocked.length === 1 ? 'An action that survives this prune still names' : 'Actions that survive this prune still name'} `
@@ -40,7 +40,7 @@ export function nextEditableSelection(
40
40
  const base = selection.set ?? [...current]
41
41
  const removed = new Set(selection.remove ?? [])
42
42
  const next = new Set([...base, ...(selection.add ?? [])].filter((name) => !removed.has(name)))
43
- return [...next].sort()
43
+ return [...next].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
44
44
  }
45
45
 
46
46
  function api(ctx: CommandContext): BlueprintAuthoringApi {
@@ -93,7 +93,7 @@ export const blueprintEditable: Command = {
93
93
  // the way out. Mapping them through the property ids again read every one
94
94
  // as unknown and reported an empty set — which would have let `--add` post
95
95
  // a set containing only the addition, silently dropping the rest.
96
- const current = [...detail.editableProperties].sort()
96
+ const current = [...detail.editableProperties].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
97
97
 
98
98
  if (!hasSet && add.length === 0 && remove.length === 0) {
99
99
  return {
@@ -119,7 +119,7 @@ export const blueprintEditable: Command = {
119
119
  `${unknown.join(', ')} ${unknown.length === 1 ? 'is not a property' : 'are not properties'} of "${apiName}".`,
120
120
  {
121
121
  code: 'USAGE',
122
- hint: `Properties: ${[...known].sort().join(', ')}`,
122
+ hint: `Properties: ${[...known].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).join(', ')}`,
123
123
  },
124
124
  )
125
125
  }
@@ -315,7 +315,7 @@ async function apply(project: LocalProject, target: SyncTarget, opts: ApplyOptio
315
315
  if (!result.ok && fallback?.when.test(result.output)) {
316
316
  // An empty fallback means there was nothing more to do.
317
317
  result = { ok: true, output: '' }
318
- for (const args of fallback.then) {
318
+ for (const args of fallback.steps) {
319
319
  result = await runCli(project, args, opts.auth)
320
320
  if (!result.ok) break
321
321
  }
@@ -723,7 +723,7 @@ async function show(ctx: CommandContext) {
723
723
  const checks = lastChecks(view)
724
724
  const items = view.items.map((item) => {
725
725
  const base = bases[item.ref]!
726
- const paths = [...new Set([...Object.keys(base.files), ...Object.keys(item.files)])].sort()
726
+ const paths = [...new Set([...Object.keys(base.files), ...Object.keys(item.files)])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
727
727
  const files = paths.map((path) => ({ path, base: base.files[path] ?? null, local: item.files[path] ?? null })).filter((f) => f.base !== f.local || !base.known)
728
728
  const change = item.deleted ? ('deleted' as const) : item.baseRevision ? ('changed' as const) : ('new' as const)
729
729
  const title = titleOf(item, titles)
@@ -809,7 +809,7 @@ async function itemState(project: LocalProject, link: SyncLink, ref: string) {
809
809
  const item = remote.items.find((i) => i.ref === ref) ?? null
810
810
  const entry = lock?.workspace === link.workspace ? lock.items[ref] : undefined
811
811
  if (!row && !item && !entry) throw new UsageError(`not in the workspace or the project: ${ref}`, 'list items with `frontera local sync pull --preview`')
812
- const paths = [...new Set([...Object.keys(item?.files ?? {}), ...Object.keys(entry?.files ?? {}), ...(entry?.conflict?.files ?? [])])].sort()
812
+ const paths = [...new Set([...Object.keys(item?.files ?? {}), ...Object.keys(entry?.files ?? {}), ...(entry?.conflict?.files ?? [])])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
813
813
  const base = baseFiles(entry, (p) => readBase(project.root, p))
814
814
  const local = localFiles(project.root, paths)
815
815
  const theirs = item?.files ?? {}
@@ -476,7 +476,7 @@ export function renderCommandHelp(meta: CommandMeta): string {
476
476
  // Command flags first, then the ones every command shares — what is specific
477
477
  // to this command is what the reader came for.
478
478
  const shared = new Set(Object.keys(GLOBAL_FLAGS))
479
- const own = Object.keys(spec).filter((f) => !shared.has(f)).sort()
479
+ const own = Object.keys(spec).filter((f) => !shared.has(f)).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
480
480
  const short = Object.entries(aliasesFor(meta)).reduce<Record<string, string>>((acc, [s, long]) => {
481
481
  acc[long] = s
482
482
  return acc
@@ -545,7 +545,7 @@ export function describeCommand(meta: CommandMeta): CommandDescriptor {
545
545
  ...(meta.reserved ? { unavailableReason: meta.reserved } : {}),
546
546
  args: meta.args.map((a) => ({ ...a })),
547
547
  flags: Object.keys(spec)
548
- .sort()
548
+ .sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
549
549
  .map((name) => ({
550
550
  name,
551
551
  type: spec[name]!,
@@ -59,12 +59,17 @@ async function probe(cwd: string, claudePath: string, log?: Logger): Promise<For
59
59
  const session = query({
60
60
  // Never yields: the initialization response is all this call is for, and
61
61
  // a prompt that resolved would send a turn to the model.
62
- prompt: (async function* () {
63
- await new Promise<void>((resolve) => {
64
- if (abort.signal.aborted) resolve()
65
- else abort.signal.addEventListener('abort', () => resolve(), { once: true })
66
- })
67
- })(),
62
+ prompt: {
63
+ [Symbol.asyncIterator]() {
64
+ return {
65
+ next: () => new Promise<IteratorResult<never>>((resolve) => {
66
+ const done = () => resolve({ done: true, value: undefined })
67
+ if (abort.signal.aborted) done()
68
+ else abort.signal.addEventListener('abort', done, { once: true })
69
+ }),
70
+ }
71
+ },
72
+ },
68
73
  options: {
69
74
  cwd,
70
75
  pathToClaudeCodeExecutable: claudePath,
@@ -58,7 +58,7 @@ function readSkillDir(dir: string): Array<{ dirName: string; path: string; conte
58
58
  return []
59
59
  }
60
60
  const out: Array<{ dirName: string; path: string; contents: string }> = []
61
- for (const entry of [...entries].sort()) {
61
+ for (const entry of [...entries].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
62
62
  const path = join(dir, entry, 'SKILL.md')
63
63
  try {
64
64
  out.push({ dirName: entry, path, contents: readFileSync(path, 'utf8') })
@@ -92,14 +92,14 @@ function pluginSkillRoots(configDir: string): Array<{ plugin: string; dir: strin
92
92
  } catch {
93
93
  return []
94
94
  }
95
- for (const marketplace of marketplaces.sort()) {
95
+ for (const marketplace of marketplaces.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
96
96
  let plugins: string[]
97
97
  try {
98
98
  plugins = readdirSync(join(cache, marketplace))
99
99
  } catch {
100
100
  continue
101
101
  }
102
- for (const plugin of plugins.sort()) {
102
+ for (const plugin of plugins.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
103
103
  let versions: string[]
104
104
  try {
105
105
  versions = readdirSync(join(cache, marketplace, plugin))
@@ -107,7 +107,7 @@ function pluginSkillRoots(configDir: string): Array<{ plugin: string; dir: strin
107
107
  continue
108
108
  }
109
109
  // Newest version wins when a cache holds more than one.
110
- for (const version of versions.sort().reverse()) {
110
+ for (const version of versions.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).reverse()) {
111
111
  const dir = join(cache, marketplace, plugin, version, 'skills')
112
112
  try {
113
113
  if (statSync(dir).isDirectory()) {
package/src/heal.ts CHANGED
@@ -491,7 +491,7 @@ function vendorSdkPackage(dir: string, pkg: AppSdkPackage): void {
491
491
  */
492
492
  export function vendoredManifest(pkg: AppSdkPackage, files: string[]): Record<string, unknown> {
493
493
  const exports: Record<string, unknown> = { './package.json': './package.json' }
494
- for (const rel of files.sort()) {
494
+ for (const rel of files.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
495
495
  if (!/\.tsx?$/.test(rel)) continue
496
496
  exports[`./${rel.replace(/\.tsx?$/, '')}`] = { types: `./${rel}`, import: `./${rel}` }
497
497
  }
package/src/kit.ts CHANGED
@@ -135,7 +135,7 @@ export interface RenderedProject {
135
135
  /** One source, two native projections — Codex reads neither host's other tree. */
136
136
  export function renderKit(kit: KitAssets = KIT): RenderedProject {
137
137
  const generated: Record<string, string> = {}
138
- for (const rel of Object.keys(kit.assets).sort()) {
138
+ for (const rel of Object.keys(kit.assets).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
139
139
  const content = kit.assets[rel]!
140
140
  if (kit.hosts.codex) generated[join('.agents', 'skills', rel)] = content
141
141
  if (kit.hosts.claudeCode) generated[join('.claude', 'skills', rel)] = content
@@ -88,7 +88,7 @@ export async function applyAgentGrants(input: {
88
88
  continue
89
89
  }
90
90
  // `read` is implied by the others (`read_sensitive`, `act`), as the runtime stores the grant.
91
- wanted.set(ref.name, [...new Set(['read', ...(Array.isArray(g.permissions) ? g.permissions : [])])].sort())
91
+ wanted.set(ref.name, [...new Set(['read', ...(Array.isArray(g.permissions) ? g.permissions : [])])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)))
92
92
  if (input.keepHeld || !Array.isArray(g.permissions)) keep.add(ref.name)
93
93
  }
94
94
 
@@ -103,7 +103,7 @@ export async function applyAgentGrants(input: {
103
103
  const onRuntime = new Set(held.ok ? list<{ apiName?: string }>(held.data).flatMap((t) => (t.apiName ? [t.apiName] : [])) : [])
104
104
  const current = await call(creds.orgKey, `/v1/blueprint/grants/agent/${encodeURIComponent(agent.id)}`)
105
105
  if (!current.ok) return { failures: [...failures, ` agent/${slug}: its Blueprint access could not be read: ${brief(current)}`], notes }
106
- const has = new Map(list<{ objectTypeApiName: string; permissions?: string[] }>(current.data).map((g) => [g.objectTypeApiName, [...new Set(['read', ...(g.permissions ?? [])])].sort()]))
106
+ const has = new Map(list<{ objectTypeApiName: string; permissions?: string[] }>(current.data).map((g) => [g.objectTypeApiName, [...new Set(['read', ...(g.permissions ?? [])])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))]))
107
107
 
108
108
  let granted = 0
109
109
  const acting: string[] = []
@@ -111,7 +111,7 @@ export async function applyAgentGrants(input: {
111
111
  for (const [apiName, stated] of wanted) {
112
112
  const held = has.get(apiName) ?? []
113
113
  const extras = keep.has(apiName) ? held.filter((p) => !stated.includes(p)) : []
114
- const permissions = extras.length ? [...stated, ...extras].sort() : stated
114
+ const permissions = extras.length ? [...stated, ...extras].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)) : stated
115
115
  const why = input.leftOff(apiName)
116
116
  if (why) {
117
117
  notes.push(` warning: Context and data: ${apiName} is left off here (${why}), so ${slug} can’t read it locally`)
@@ -141,7 +141,7 @@ export async function applyAgentGrants(input: {
141
141
  const res = await call(creds.orgKey, `/v1/blueprint/grants/agent/${encodeURIComponent(agent.id)}/${encodeURIComponent(apiName)}`, 'DELETE')
142
142
  if (!res.ok) failures.push(` objectType/${apiName}: could not be revoked: ${brief(res)}`)
143
143
  }
144
- if (granted) notes.push(` Context and data: ${granted} object type${granted === 1 ? '' : 's'}${acting.length ? `; ${slug} can submit Actions on ${acting.sort().join(', ')} (act)` : ''}`)
144
+ if (granted) notes.push(` Context and data: ${granted} object type${granted === 1 ? '' : 's'}${acting.length ? `; ${slug} can submit Actions on ${acting.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).join(', ')} (act)` : ''}`)
145
145
  if (kept.length) notes.push(` Context and data: kept what ${slug}'s grants already held where its bindings.json names no permissions: ${kept.join('; ')}`)
146
146
  return { failures, notes }
147
147
  }
@@ -91,7 +91,7 @@ export function contentHash(project: LocalProject, target: SyncTarget): string |
91
91
  // shared files it imports from outside functions/.
92
92
  if (target.kind === 'function') {
93
93
  hash.update(treeHash(join(project.root, 'functions')) ?? '').update(treeHash(join(project.root, 'vendor')) ?? '')
94
- for (const path of importedFiles(project.root, target.file).filter((p) => !p.startsWith('functions/')).sort()) hash.update(path).update('\0').update(fileDigest(join(project.root, path)) ?? '')
94
+ for (const path of importedFiles(project.root, target.file).filter((p) => !p.startsWith('functions/')).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) hash.update(path).update('\0').update(fileDigest(join(project.root, path)) ?? '')
95
95
  }
96
96
  // Context and data the pull could not read is assumed on the project's object types: a new one changes it.
97
97
  if (target.kind === 'bindings' && /"blueprintUnread"\s*:\s*true/.test(readFileSync(join(project.root, target.file), 'utf8'))) {
@@ -154,7 +154,7 @@ export async function applyApp(root: string, slug: string, call: Call, provides:
154
154
  const gone = await call('DELETE', `/v1/functions/${encodeURIComponent(fn)}/app-bindings/${encodeURIComponent(slug)}`)
155
155
  if (gone.status >= 400 && gone.status !== 404) lines.push(` warning: Function ${fn} could not be unbound from it (${errorOf(gone)})`)
156
156
  }
157
- lines.unshift(` saved App ${name}${named.size ? ` (may start ${[...named].sort().join(', ')})` : ''}`)
157
+ lines.unshift(` saved App ${name}${named.size ? ` (may start ${[...named].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).join(', ')})` : ''}`)
158
158
  return { ok: true, output: lines.join('\n') }
159
159
  }
160
160
 
@@ -730,7 +730,7 @@ export function editableSets(dir: string, subjects: ReadonlySet<string> | null):
730
730
  try {
731
731
  const doc = parseYaml(readFileSync(join(at, name), 'utf8')) as { apiName?: unknown; backing?: { editable?: unknown } } | null
732
732
  const editable = doc?.backing?.editable
733
- if (typeof doc?.apiName === 'string' && (!subjects || subjects.has(doc.apiName)) && Array.isArray(editable) && editable.length) out.set(doc.apiName, editable.map(String).sort())
733
+ if (typeof doc?.apiName === 'string' && (!subjects || subjects.has(doc.apiName)) && Array.isArray(editable) && editable.length) out.set(doc.apiName, editable.map(String).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)))
734
734
  } catch {
735
735
  // A file that does not parse is refused by the apply itself.
736
736
  }
@@ -1420,7 +1420,7 @@ export async function applyBindings(project: LocalProject, target: Extract<SyncT
1420
1420
  const unread = !Array.isArray(doc.blueprint) && (doc as { blueprintUnread?: unknown }).blueprintUnread === true
1421
1421
  if (Array.isArray(doc.blueprint) || unread) {
1422
1422
  const types = projectObjectTypes(project.root)
1423
- const keptHere = [...types].filter(([, type]) => !objectTypeFit(type, () => true).leftOff).map(([apiName]) => apiName).sort()
1423
+ const keptHere = [...types].filter(([, type]) => !objectTypeFit(type, () => true).leftOff).map(([apiName]) => apiName).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
1424
1424
  const granted = await applyAgentGrants({
1425
1425
  slug: target.slug,
1426
1426
  grants: unread ? keptHere.map((apiName) => ({ objectType: `objectType/${apiName}`, permissions: ['read'] })) : (doc.blueprint as AgentGrant[]),
@@ -299,10 +299,10 @@ export function commandFor(target: SyncTarget, profiles: SyncProfiles): string[]
299
299
  }
300
300
  }
301
301
 
302
- /** When the first command fails with `when`, run `then` instead. */
302
+ /** When the first command fails with `when`, run `steps` instead. */
303
303
  export interface Fallback {
304
304
  when: RegExp
305
- then: string[][]
305
+ steps: string[][]
306
306
  }
307
307
 
308
308
  /**
@@ -310,7 +310,7 @@ export interface Fallback {
310
310
  * Agent applied for the first time is created, then applied (`agent apply`
311
311
  * stages onto an existing draft); a dataset that exists already gets its CSV
312
312
  * as a new snapshot (`dataset create` refuses a name in use). A dataset with
313
- * no CSV that exists already has nothing more to load: an empty `then`.
313
+ * no CSV that exists already has nothing more to load: an empty `steps`.
314
314
  *
315
315
  * Only rows reach an existing dataset. A CSV with empty values a column of its
316
316
  * revision does not allow is refused; `local dev` then cuts the dataset's next
@@ -322,14 +322,14 @@ export function fallbackFor(target: SyncTarget, profiles: SyncProfiles, root: st
322
322
  if (target.kind === 'agent') {
323
323
  // Created with its name, or every list reads the slug until it is published.
324
324
  const named = createFlags(agentIdentity(root, target.file))
325
- return { when: /NOT_FOUND/, then: [['agent', 'create', target.slug, ...named, '--profile', profiles.workspace], commandFor(target, profiles)] }
325
+ return { when: /NOT_FOUND/, steps: [['agent', 'create', target.slug, ...named, '--profile', profiles.workspace], commandFor(target, profiles)] }
326
326
  }
327
327
  // A workflow archived on the runtime takes no new version: restored, then pushed (the files say it is in service).
328
- if (target.kind === 'workflow') return { when: /is archived, so it cannot/i, then: [['workflow', 'restore', target.slug, '--profile', profiles.workspace], commandFor(target, profiles)] }
328
+ if (target.kind === 'workflow') return { when: /is archived, so it cannot/i, steps: [['workflow', 'restore', target.slug, '--profile', profiles.workspace], commandFor(target, profiles)] }
329
329
  if (target.kind === 'dataset') {
330
330
  const doc = datasetDocument(root, target)
331
331
  const upload = doc?.csv ? [['dataset', 'upload', doc.apiName, doc.csv, '--profile', profiles.organization]] : []
332
- return { when: /already in use/i, then: upload }
332
+ return { when: /already in use/i, steps: upload }
333
333
  }
334
334
  return null
335
335
  }
@@ -179,7 +179,7 @@ export function documentPaths(root: string, kb: string): string[] {
179
179
  }
180
180
  }
181
181
  if (existsSync(top)) walk('', 0)
182
- return out.sort()
182
+ return out.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
183
183
  }
184
184
 
185
185
  /** The documents in a knowledge base's folder and its subfolders, by path. */
@@ -96,7 +96,7 @@ export async function pullLessons(root: string, workspace: string, ws: SyncClien
96
96
  const byAgent = new Map(rows<LessonsRow>(found).map((r) => [r.agent, r.rules]))
97
97
  const record = readRecord(root, workspace)
98
98
  const out: LessonsPulled = { written: [], removed: [], kept: [], rules: 0 }
99
- for (const slug of [...new Set(agents)].sort()) {
99
+ for (const slug of [...new Set(agents)].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
100
100
  const path = join(root, lessonsPath(slug))
101
101
  const here = existsSync(path) ? readFileSync(path, 'utf8') : null
102
102
  const editedHere = here !== null && record.files[slug] !== sha(here)
@@ -64,7 +64,7 @@ export function readLibrary(rows: LibraryRow[], directory: Directory): RemoteIte
64
64
  release: { number: r.release.number, version: r.release.version },
65
65
  ...(r.state && r.state !== 'installed' ? { state: r.state } : {}),
66
66
  ...(agent ? { agent: `agent/${agent}` } : {}),
67
- ...(made.length ? { skills: [...new Set(made)].sort() } : {}),
67
+ ...(made.length ? { skills: [...new Set(made)].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)) } : {}),
68
68
  }
69
69
  })
70
70
  const doc: LibraryFile = { installations }
@@ -145,7 +145,7 @@ export function settingsDifferences(workspace: OrgSettings, local: OrgSettings,
145
145
  if (both('voiceSpeed') && differs(workspace.voiceSpeed, local.voiceSpeed)) out.push(`Voice speed is ${workspace.voiceSpeed ?? DEFAULT} in the workspace and ${local.voiceSpeed ?? DEFAULT} here`)
146
146
  const ws = workspace.featureFlags ?? {}
147
147
  const here = local.featureFlags ?? {}
148
- for (const key of Object.keys(ws).sort()) {
148
+ for (const key of Object.keys(ws).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
149
149
  if (NOT_A_DIFFERENCE_HERE.has(key)) continue
150
150
  if (key in here && ws[key] !== here[key]) out.push(`Feature ${key} is ${onOff(ws[key])} in the workspace and ${onOff(here[key])} here`)
151
151
  }
@@ -427,9 +427,9 @@ export function itemNeeds(kind: string, files: Record<string, string>, connectio
427
427
  }
428
428
  needs.push({ kind: 'app-code', ...(external ? { external } : {}) })
429
429
  }
430
- for (const name of [...actions].sort()) needs.push({ kind: 'action', name })
430
+ for (const name of [...actions].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) needs.push({ kind: 'action', name })
431
431
  if (unnamed) needs.push({ kind: 'action-unnamed', count: unnamed })
432
- for (const name of [...plugins].sort()) needs.push({ kind: builtIns.get(name) ?? 'connection', name })
432
+ for (const name of [...plugins].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) needs.push({ kind: builtIns.get(name) ?? 'connection', name })
433
433
  return needs
434
434
  }
435
435
 
@@ -55,7 +55,7 @@ export function pluginSecretName(plugin: string, field: string): string {
55
55
 
56
56
  /** The `secret:NAME` grants a Function's source names. */
57
57
  export function functionSecretNames(source: string): string[] {
58
- return [...new Set([...source.matchAll(/['"`]secret:([A-Z][A-Z0-9_]*)['"`]/g)].map((m) => m[1]!))].sort()
58
+ return [...new Set([...source.matchAll(/['"`]secret:([A-Z][A-Z0-9_]*)['"`]/g)].map((m) => m[1]!))].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
59
59
  }
60
60
 
61
61
  /** The secrets a workflow's HTTP steps and webhook trigger name in their `auth`, at any depth. */
@@ -70,7 +70,7 @@ export function workflowSecretNames(doc: unknown): string[] {
70
70
  }
71
71
  }
72
72
  visit(doc)
73
- return [...out].sort()
73
+ return [...out].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
74
74
  }
75
75
 
76
76
  /** A Plugin setting declared empty in its file: a header or a config key, and the local secret that fills it. */
@@ -262,7 +262,7 @@ function sameBesideSecrets(text: string, base: string): boolean {
262
262
 
263
263
  function sortKeys(v: unknown): unknown {
264
264
  if (Array.isArray(v)) return v.map(sortKeys)
265
- if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v as Record<string, unknown>).sort().map((k) => [k, sortKeys((v as Record<string, unknown>)[k])]))
265
+ if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v as Record<string, unknown>).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).map((k) => [k, sortKeys((v as Record<string, unknown>)[k])]))
266
266
  return v
267
267
  }
268
268
 
@@ -70,7 +70,7 @@ const DOCS_TOOLS: StandInFile['tools'] = [
70
70
 
71
71
  /** The built-in Plugins these items need that get a stand-in (connections, the Documentation plugin), by install name. */
72
72
  export function connectionsNeeded(items: Array<Pick<RemoteItem, 'portability'>>): string[] {
73
- return [...new Set(items.flatMap((i) => (i.portability?.needs ?? []).flatMap((n) => (n.kind === 'connection' || n.kind === 'documentation' ? [n.name] : []))))].sort()
73
+ return [...new Set(items.flatMap((i) => (i.portability?.needs ?? []).flatMap((n) => (n.kind === 'connection' || n.kind === 'documentation' ? [n.name] : []))))].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
74
74
  }
75
75
 
76
76
  /** Which of those are the Documentation plugin. */
@@ -80,7 +80,7 @@ export function documentationNeeded(items: Array<Pick<RemoteItem, 'portability'>
80
80
 
81
81
  /** The workspace's analytics Plugins these items use: platform-only. */
82
82
  export function analyticsNeeded(items: Array<Pick<RemoteItem, 'portability'>>): string[] {
83
- return [...new Set(items.flatMap((i) => (i.portability?.needs ?? []).flatMap((n) => (n.kind === 'analytics' ? [n.name] : []))))].sort()
83
+ return [...new Set(items.flatMap((i) => (i.portability?.needs ?? []).flatMap((n) => (n.kind === 'analytics' ? [n.name] : []))))].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
84
84
  }
85
85
 
86
86
  // ── Platform-only Plugins ────────────────────────────────────────────────────
@@ -372,7 +372,7 @@ export function mergeItemFiles(
372
372
  theirs: Record<string, string | null>,
373
373
  opts: Omit<MergeOptions, 'offset'> = {},
374
374
  ): { files: FileMerge[]; conflicts: number } {
375
- const paths = [...new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])].sort()
375
+ const paths = [...new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
376
376
  const files: FileMerge[] = []
377
377
  let offset = 0
378
378
  for (const path of paths) {
@@ -223,7 +223,7 @@ export function hash(text: string): string {
223
223
  }
224
224
 
225
225
  export function itemHash(files: Record<string, string>): string {
226
- return hash(Object.keys(files).sort().map((p) => `${p}\0${files[p]}`).join('\0\0'))
226
+ return hash(Object.keys(files).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).map((p) => `${p}\0${files[p]}`).join('\0\0'))
227
227
  }
228
228
 
229
229
  export function readLock(root: string): SyncLock | null {
@@ -240,7 +240,7 @@ export function readLock(root: string): SyncLock | null {
240
240
 
241
241
  /** Sorted keys, two spaces, a final newline: a lock diffs cleanly in review. Always written as v2. */
242
242
  export function writeLock(root: string, lock: SyncLock): void {
243
- const items = Object.fromEntries(Object.keys(lock.items).sort().map((k) => [k, sortEntry(lock.items[k]!)]))
243
+ const items = Object.fromEntries(Object.keys(lock.items).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).map((k) => [k, sortEntry(lock.items[k]!)]))
244
244
  writeFileSync(join(root, LOCK_FILE), `${JSON.stringify({ version: 2, workspace: lock.workspace, ...(lock.pulledAt ? { pulledAt: lock.pulledAt } : {}), items }, null, 2)}\n`)
245
245
  }
246
246
 
@@ -249,7 +249,7 @@ function sortEntry(entry: LockEntry): LockEntry {
249
249
  ...(entry.id ? { id: entry.id } : {}),
250
250
  ...(entry.revision ? { revision: entry.revision } : {}),
251
251
  remote: entry.remote,
252
- files: Object.fromEntries(Object.keys(entry.files).sort().map((k) => [k, entry.files[k]!])),
252
+ files: Object.fromEntries(Object.keys(entry.files).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).map((k) => [k, entry.files[k]!])),
253
253
  ...(entry.version ? { version: entry.version } : {}),
254
254
  ...(entry.state ? { state: entry.state } : {}),
255
255
  pulledAt: entry.pulledAt,
@@ -262,7 +262,7 @@ function sortConflict(c: LockConflict): LockConflict {
262
262
  return {
263
263
  remote: c.remote,
264
264
  ...(c.revision ? { revision: c.revision } : {}),
265
- files: [...c.files].sort(),
265
+ files: [...c.files].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)),
266
266
  at: c.at,
267
267
  ...(c.count !== undefined ? { count: c.count } : {}),
268
268
  ...(c.id ? { id: c.id } : {}),
@@ -400,7 +400,7 @@ export function computePlan(root: string, remote: RemoteItem[], lock: SyncLock |
400
400
  for (const item of remote) {
401
401
  seen.add(item.ref)
402
402
  const entry = lock?.items[item.ref]
403
- const paths = [...new Set([...Object.keys(item.files), ...Object.keys(entry?.files ?? {})])].sort()
403
+ const paths = [...new Set([...Object.keys(item.files), ...Object.keys(entry?.files ?? {})])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
404
404
  const files: FileChange[] = paths.map((path) => ({ path, local: readComparable(root, path, opts.base), remote: item.files[path] ?? null }))
405
405
  // An Agent's file that neither the base nor the workspace has but the folder does (its first tools, in a new
406
406
  // `bindings.json`) is a change here too, as push reads it: otherwise status says nothing while push proposes it.
@@ -553,7 +553,7 @@ export function applyItems(root: string, workspace: string, items: RemoteItem[],
553
553
  for (const item of items) {
554
554
  const previous = next.items[item.ref]
555
555
  if (opts.merge?.has(item.ref)) {
556
- const paths = [...new Set([...Object.keys(item.files), ...Object.keys(previous?.files ?? {})])].sort()
556
+ const paths = [...new Set([...Object.keys(item.files), ...Object.keys(previous?.files ?? {})])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
557
557
  const result = mergeItemFiles(baseFiles(previous, opts.base), localFiles(root, paths), item.files)
558
558
  if (result.conflicts) {
559
559
  const files = result.files.filter((f) => f.conflicts.length).map((f) => f.path)
@@ -633,7 +633,7 @@ export function refreshConflict(root: string, workspace: string, item: RemoteIte
633
633
  const previous = next.items[item.ref]
634
634
  const remote = itemHash(item.files)
635
635
  if (!previous?.conflict || previous.conflict.remote === remote) return { lock: next, outcome: 'unchanged', conflicts: previous?.conflict?.count ?? 0, written: [], removed: [] }
636
- const paths = [...new Set([...Object.keys(item.files), ...Object.keys(previous.files), ...previous.conflict.files])].sort()
636
+ const paths = [...new Set([...Object.keys(item.files), ...Object.keys(previous.files), ...previous.conflict.files])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
637
637
  const result = mergeItemFiles(baseFiles(previous, base), localFiles(root, paths), item.files)
638
638
  const now = new Date().toISOString()
639
639
  const written: string[] = []
@@ -673,7 +673,7 @@ export type Resolution = { edited: true } | { keep?: Side; picks?: Map<number, C
673
673
  export function resolveItem(root: string, workspace: string, item: RemoteItem, lock: SyncLock | null, how: Resolution, base?: (path: string) => string | null): { written: string[]; removed: string[]; lock: SyncLock } {
674
674
  const next: SyncLock = { version: 2, workspace, items: { ...(lock?.workspace === workspace ? lock.items : {}) } }
675
675
  const previous = next.items[item.ref]
676
- const paths = [...new Set([...Object.keys(item.files), ...Object.keys(previous?.files ?? {}), ...(previous?.conflict?.files ?? [])])].sort()
676
+ const paths = [...new Set([...Object.keys(item.files), ...Object.keys(previous?.files ?? {}), ...(previous?.conflict?.files ?? [])])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
677
677
  const written: string[] = []
678
678
  const removed: string[] = []
679
679
  if (!('edited' in how)) {
@@ -228,7 +228,7 @@ export function itemFiles(root: string, kind: SyncKind, name: string, entry?: Lo
228
228
  // What the last pull wrote for it comes along when it is still here (an agent's files are fixed; the rest may not be).
229
229
  if (kind !== 'agent' && kind !== 'knowledge') for (const p of Object.keys(entry?.files ?? {})) if (!paths.includes(p)) paths.push(p)
230
230
  const files: Record<string, string> = {}
231
- for (const path of [...new Set(paths)].sort()) {
231
+ for (const path of [...new Set(paths)].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
232
232
  if (path.endsWith('.workspace') || assets.includes(path)) continue
233
233
  const text = readLocal(root, path)
234
234
  if (text === null) continue
@@ -372,7 +372,7 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
372
372
  const here = projectRefs(root)
373
373
  const refs = new Set<string>([...plan.map((p) => p.ref), ...here.keys()])
374
374
  const out: PushItem[] = []
375
- for (const ref of [...refs].sort()) {
375
+ for (const ref of [...refs].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
376
376
  const slash = ref.indexOf('/')
377
377
  const kind = (byPlan.get(ref)?.kind ?? here.get(ref)?.kind ?? ref.slice(0, slash)) as SyncKind
378
378
  const name = byPlan.get(ref)?.name ?? here.get(ref)?.name ?? ref.slice(slash + 1)
@@ -404,7 +404,7 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
404
404
  if (status === 'same' && Object.keys(files).length === 0) continue
405
405
  const isNew = !entry && !theirs
406
406
  const baseFiles: Record<string, string> = !isNew && theirs && entry && itemHash(theirs.files) === entry.remote ? theirs.files : {}
407
- const diffPaths = [...new Set([...Object.keys(baseFiles), ...Object.keys(files)])].sort()
407
+ const diffPaths = [...new Set([...Object.keys(baseFiles), ...Object.keys(files)])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
408
408
  const diff: PushFile[] = diffPaths.map((path) => ({ path, base: baseFiles[path] ?? null, local: files[path] ?? null })).filter((f) => f.base !== f.local)
409
409
  const item: PushItem = {
410
410
  ref,
@@ -629,7 +629,7 @@ export function recordRequest(root: string, workspace: string, view: RequestView
629
629
  const file = readRequests(root, workspace)
630
630
  const now = (opts.now ?? new Date()).toISOString()
631
631
  const before = file.requests[view.request.id]
632
- const refs = view.items.map((i) => i.ref).sort()
632
+ const refs = view.items.map((i) => i.ref).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
633
633
  const titles = Object.fromEntries(refs.map((r) => [r, opts.titles?.[r] ?? before?.titles[r] ?? r]))
634
634
  file.requests[view.request.id] = {
635
635
  id: view.request.id,
@@ -716,7 +716,7 @@ export function sameDocument(path: string, a: string, b: string): boolean {
716
716
  /** Quality checks as a set: by question, each one's lists in order of their words. */
717
717
  function checkSet(text: string): unknown {
718
718
  return readQualityChecks(text)
719
- .map((c) => ({ ...c, documents: [...(c.documents ?? [])].sort(), keywords: [...(c.keywords ?? [])].sort() }))
719
+ .map((c) => ({ ...c, documents: [...(c.documents ?? [])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)), keywords: [...(c.keywords ?? [])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)) }))
720
720
  .sort((x, y) => x.question.localeCompare(y.question))
721
721
  }
722
722
 
@@ -375,7 +375,7 @@ function finish(it: RemoteItem, directory: Directory, docs: Array<{ path: string
375
375
  }
376
376
  it.files[path] = format === 'json' ? json(named) : stringifyYaml(named, { lineWidth: 0 })
377
377
  }
378
- it.requires = [...requires].sort()
378
+ it.requires = [...requires].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
379
379
  return it
380
380
  }
381
381
 
@@ -408,7 +408,7 @@ export type AgentContext = { grants: Grants; note?: string } | { unread: string
408
408
 
409
409
  const asGrants = (granted: Array<{ objectTypeApiName?: string; permissions?: string[] }>): Grants =>
410
410
  granted
411
- .flatMap((g) => (typeof g.objectTypeApiName === 'string' ? [{ objectType: `objectType/${g.objectTypeApiName}`, permissions: [...(g.permissions?.length ? g.permissions : ['read'])].sort() }] : []))
411
+ .flatMap((g) => (typeof g.objectTypeApiName === 'string' ? [{ objectType: `objectType/${g.objectTypeApiName}`, permissions: [...(g.permissions?.length ? g.permissions : ['read'])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)) }] : []))
412
412
  .sort((a, b) => a.objectType.localeCompare(b.objectType))
413
413
 
414
414
  const STEWARD_FIX = 'An organization key made by a Blueprint Steward with organization update reads it, and so does the workspace key on a newer platform'
@@ -866,7 +866,7 @@ async function readFunctions(source: SyncSource, lists: Lists, opts: ReadOptions
866
866
  const keep = imported.filter((p) => p.startsWith('functions/'))
867
867
  const test = `functions/${row.slug}.test.ts`
868
868
  if (files.has(test)) keep.push(test)
869
- const beyond = imported.filter((p) => !p.startsWith('functions/') && !p.startsWith('vendor/')).sort()
869
+ const beyond = imported.filter((p) => !p.startsWith('functions/') && !p.startsWith('vendor/')).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
870
870
  const shared = beyond.filter(isSharedFunctionFile)
871
871
  const outside = beyond.filter((p) => !isSharedFunctionFile(p))
872
872
  for (const p of [...keep, ...shared]) it.files[p] = files.get(p)!
@@ -936,7 +936,7 @@ export function noteSharedFunctionFiles(items: RemoteItem[], warnings: string[])
936
936
  for (const it of items) for (const path of Object.keys(it.files)) holders.set(path, [...(holders.get(path) ?? []), it])
937
937
  for (const [path, list] of holders) {
938
938
  if (list.length < 2 || new Set(list.map((i) => i.files[path])).size < 2) continue
939
- const names = list.map((i) => i.name).sort()
939
+ const names = list.map((i) => i.name).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
940
940
  const and = (words: string[]) => (words.length < 2 ? words.join('') : `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`)
941
941
  for (const it of list) {
942
942
  const others = names.filter((n) => n !== it.name)
@@ -1202,7 +1202,7 @@ export function objectSetItems(sets: ObjectSetRow[], directory: Directory, objec
1202
1202
  it.unresolved.push(...unresolved.map((u) => ({ ...u, path: `blueprint/object-sets/${name}.yaml: ${u.path}` })))
1203
1203
  it.files[`blueprint/object-sets/${name}.yaml`] = stringifyYaml(named, { lineWidth: 0 })
1204
1204
  const others = referencesIn(named).filter((r) => r.kind === 'objectSet' && r.name !== name).map((r) => itemRef(r.kind, r.name))
1205
- it.requires = [itemRef('objectType', set.objectType), ...new Set(others)].sort()
1205
+ it.requires = [itemRef('objectType', set.objectType), ...new Set(others)].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
1206
1206
  if (set.kind === 'static') {
1207
1207
  const n = set.staticPks?.length ?? 0
1208
1208
  it.notes.push(`Lists ${n} ${set.objectType} record${n === 1 ? '' : 's'} by key; locally it holds those of them this runtime’s data has`)
@@ -1595,7 +1595,7 @@ async function readEvaluations(source: SyncSource, lists: Lists, directory: Dire
1595
1595
  const file = `agents/${slug}/evals/${slugify(ds.name)}.yaml`
1596
1596
  const header = `# Test cases from the platform's evaluation dataset "${ds.name}" (frontera local sync).\n`
1597
1597
  const doc = { ...(isRegression ? { regression: true } : {}), cases }
1598
- out.push(item('evaluation', `${slug}.${slugify(ds.name)}`, `${ds.name} → ${slug}`, { [file]: header + stringifyYaml(doc, { lineWidth: 0 }) }, { notes, requires: [...requires].sort() }))
1598
+ out.push(item('evaluation', `${slug}.${slugify(ds.name)}`, `${ds.name} → ${slug}`, { [file]: header + stringifyYaml(doc, { lineWidth: 0 }) }, { notes, requires: [...requires].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)) }))
1599
1599
  }
1600
1600
  }
1601
1601
  return out