@voxgig/sdkgen 4.23.0 → 4.24.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 (163) hide show
  1. package/bin/voxgig-sdkgen +18 -2
  2. package/dist/action/doctor.d.ts +1 -1
  3. package/dist/action/doctor.js +12 -10
  4. package/dist/action/doctor.js.map +1 -1
  5. package/dist/action/edition.js +4 -0
  6. package/dist/action/edition.js.map +1 -1
  7. package/dist/action/feature.js +5 -1
  8. package/dist/action/feature.js.map +1 -1
  9. package/dist/action/remove.d.ts +16 -0
  10. package/dist/action/remove.js +345 -0
  11. package/dist/action/remove.js.map +1 -0
  12. package/dist/action/target.js +6 -1
  13. package/dist/action/target.js.map +1 -1
  14. package/dist/cmp/Deploy.js +9 -6
  15. package/dist/cmp/Deploy.js.map +1 -1
  16. package/dist/helpers/semver.js +3 -1
  17. package/dist/helpers/semver.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +2 -2
  20. package/project/.sdk/tm/c/core/context.c +3 -1
  21. package/project/.sdk/tm/c/feature/paging.c +37 -3
  22. package/project/.sdk/tm/c/feature/test.c +51 -27
  23. package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
  24. package/project/.sdk/tm/c/utility/transform_request.c +25 -3
  25. package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
  26. package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
  27. package/project/.sdk/tm/cpp/core/types.hpp +1 -1
  28. package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
  29. package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
  30. package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
  31. package/project/.sdk/tm/csharp/core/Context.cs +1 -1
  32. package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
  33. package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
  34. package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
  35. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
  36. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
  37. package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
  38. package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
  39. package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
  40. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
  41. package/project/.sdk/tm/go/core/context.go +13 -13
  42. package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
  43. package/project/.sdk/tm/go/feature/test_feature.go +3 -15
  44. package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
  45. package/project/.sdk/tm/go/test/feature_test.go +54 -0
  46. package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
  47. package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
  48. package/project/.sdk/tm/go/utility/fetcher.go +41 -20
  49. package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
  50. package/project/.sdk/tm/go/utility/transform_request.go +22 -3
  51. package/project/.sdk/tm/java/core/Context.java +2 -1
  52. package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
  53. package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
  54. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
  55. package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
  56. package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
  57. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
  58. package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
  59. package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
  60. package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
  61. package/project/.sdk/tm/js/src/Context.js +7 -2
  62. package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
  63. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
  64. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
  65. package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
  66. package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
  67. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
  68. package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
  69. package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
  70. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
  71. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
  72. package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
  73. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
  74. package/project/.sdk/tm/lua/core/context.lua +8 -1
  75. package/project/.sdk/tm/lua/core/control.lua +3 -0
  76. package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
  77. package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
  78. package/project/.sdk/tm/lua/test/omni.lua +8 -1
  79. package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
  80. package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
  81. package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
  82. package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
  83. package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
  84. package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
  85. package/project/.sdk/tm/perl/core/context.pm +2 -1
  86. package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
  87. package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
  88. package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
  89. package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
  90. package/project/.sdk/tm/perl/t/omni.pm +7 -1
  91. package/project/.sdk/tm/perl/t/pipeline.t +124 -27
  92. package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
  93. package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
  94. package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
  95. package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
  96. package/project/.sdk/tm/php/core/Context.php +5 -1
  97. package/project/.sdk/tm/php/core/Control.php +5 -0
  98. package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
  99. package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
  100. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
  101. package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
  102. package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
  103. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
  104. package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
  105. package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
  106. package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
  107. package/project/.sdk/tm/py/pkg/core/context.py +2 -1
  108. package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
  109. package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
  110. package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
  111. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
  112. package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
  113. package/project/.sdk/tm/py/test/test_feature.py +27 -0
  114. package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
  115. package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
  116. package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
  117. package/project/.sdk/tm/rb/core/context.rb +1 -1
  118. package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
  119. package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
  120. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
  121. package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
  122. package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
  123. package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
  124. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
  125. package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
  126. package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
  127. package/project/.sdk/tm/rust/core/context.rs +2 -6
  128. package/project/.sdk/tm/rust/feature/paging.rs +44 -3
  129. package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
  130. package/project/.sdk/tm/rust/feature/test.rs +66 -33
  131. package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
  132. package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
  133. package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
  134. package/project/.sdk/tm/scala/core/Context.scala +2 -1
  135. package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
  136. package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
  137. package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
  138. package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
  139. package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
  141. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
  142. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
  143. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
  144. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
  145. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
  146. package/project/.sdk/tm/ts/src/Context.ts +7 -2
  147. package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
  148. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
  149. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
  150. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
  151. package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
  152. package/project/.sdk/tm/zig/core/context.zig +1 -1
  153. package/project/.sdk/tm/zig/core/utility.zig +19 -5
  154. package/project/.sdk/tm/zig/feature/paging.zig +34 -4
  155. package/project/.sdk/tm/zig/feature/test.zig +54 -26
  156. package/project/sdkgen-package.json +1 -1
  157. package/src/action/doctor.ts +12 -9
  158. package/src/action/edition.ts +8 -0
  159. package/src/action/feature.ts +7 -1
  160. package/src/action/remove.ts +492 -0
  161. package/src/action/target.ts +8 -1
  162. package/src/cmp/Deploy.ts +11 -6
  163. package/src/helpers/semver.ts +3 -1
@@ -0,0 +1,492 @@
1
+ import { kindCollection } from '../helpers/kindCollection'
2
+
3
+ import Path from 'node:path'
4
+
5
+ import { KIT } from '../types'
6
+
7
+ import type {
8
+ ActionContext,
9
+ ActionResult,
10
+ } from '../types'
11
+
12
+ import { SdkGenError } from '../utility'
13
+
14
+ import { definitionPath, definitionFolder, indexName } from '../helpers/definition'
15
+
16
+ import { findFeatureSources, BASE_FEATURE } from '../helpers/featureSource'
17
+
18
+ import { ITEM_NAME_RE } from '../helpers/manifest'
19
+
20
+ import { isJunk } from '../helpers/junk'
21
+
22
+ import { kindDef, kindTrees } from './kind'
23
+
24
+ import { resolveSource, recordedRef } from './resolve'
25
+
26
+ import { removeIndexEntries } from './action'
27
+
28
+ import { doctor } from './doctor'
29
+
30
+
31
+ // Everything one `remove` would touch, decided before anything is written.
32
+ type RemovePlan = {
33
+ kind: string
34
+ name: string
35
+
36
+ // Project-relative paths, files only.
37
+ files: string[]
38
+
39
+ // Trees to drop once their files are gone.
40
+ dirs: string[]
41
+
42
+ // Whether the index lists the item.
43
+ indexed: boolean
44
+
45
+ // The generated output directory beside `.sdk`, when the caller asked
46
+ // for it and it exists.
47
+ output?: string
48
+
49
+ // Files `add` did not write as they stand: forked, edited, stale or
50
+ // project-owned. The removal refuses on any of these unless forced.
51
+ refused: string[]
52
+
53
+ // The subset of `refused` that is an ALIASED item's own model file. Same
54
+ // refusal, different advice — see the message in kindRemove.
55
+ aliased: string[]
56
+
57
+ // Things the caller must finish by hand.
58
+ notes: string[]
59
+ }
60
+
61
+
62
+ async function kind_remove(
63
+ kind: string, names: string[], actx: ActionContext,
64
+ ): Promise<ActionResult> {
65
+ const log = actx.log
66
+ const dryrun = !!actx.opts?.dryrun
67
+ const force = true === actx.flags?.force
68
+ const deleteOutput = true === actx.flags?.deleteOutput
69
+
70
+ kindDef(kind)
71
+
72
+ if (0 === names.length) {
73
+ throw new SdkGenError(kind + ' remove: nothing named' +
74
+ '\n usage: voxgig-sdkgen ' + kind + ' remove <name>[,<name>...]')
75
+ }
76
+
77
+ log.info({
78
+ point: 'remove-start', kind, names,
79
+ note: (dryrun ? '** DRY RUN ** ' : '') + kind + ' remove ' + names.join(',')
80
+ })
81
+
82
+ const plans: RemovePlan[] = []
83
+ for (const name of names) {
84
+ plans.push(await planRemove(kind, name, actx, deleteOutput))
85
+ }
86
+
87
+ const refused = plans.filter((p) => 0 < p.refused.length)
88
+
89
+ if (0 < refused.length && !force) {
90
+ // Differentiating an alias's model file is what an alias is for, so the
91
+ // standard advice names the place that file already is.
92
+ const aliased = refused.flatMap((p) => p.aliased)
93
+
94
+ throw new SdkGenError(
95
+ kind + ' remove: refusing to delete what `' + kind + ' add` did not write' +
96
+ refused.map((p) => '\n ' + p.name + ':' +
97
+ p.refused.map((r) => '\n ' + r).join('')).join('') +
98
+ (0 < aliased.length ?
99
+ ('\n ' + aliased.join(', ') + ' is an ALIAS\'s own model file: the' +
100
+ ' scaffold has none to compare it with, so differentiating it is' +
101
+ ' expected and there is nowhere else to move it. Pass --force to' +
102
+ ' delete it with the alias.') : '') +
103
+ '\n move any project decision into .sdk/model/, then run again;' +
104
+ ' or pass --force to delete these too')
105
+ }
106
+
107
+ const removed: string[] = []
108
+
109
+ for (const plan of plans) {
110
+ for (const r of plan.refused) {
111
+ log.warn({
112
+ point: 'remove-forced', kind, [kind]: plan.name, file: r,
113
+ note: plan.name + ': --force, deleting ' + r
114
+ })
115
+ }
116
+
117
+ for (const note of plan.notes) {
118
+ log.warn({ point: 'remove-note', kind, [kind]: plan.name, note })
119
+ }
120
+
121
+ removed.push(...applyRemove(plan, actx, dryrun))
122
+ }
123
+
124
+ log.info({
125
+ point: 'remove-end', kind, names, count: removed.length,
126
+ note: dryrun ?
127
+ ('** DRY RUN ** ' + removed.length + ' path(s) would be removed; nothing was written') :
128
+ ('removed ' + removed.length + ' path(s)')
129
+ })
130
+
131
+ return {
132
+ report: {
133
+ ok: true,
134
+ kind,
135
+ names,
136
+ removed,
137
+ refused: plans.flatMap((p) => p.refused),
138
+ notes: plans.flatMap((p) => p.notes),
139
+ dryrun,
140
+ }
141
+ }
142
+ }
143
+
144
+
145
+ async function planRemove(
146
+ kind: string, name: string, actx: ActionContext, deleteOutput: boolean,
147
+ ): Promise<RemovePlan> {
148
+ const fs = actx.fs()
149
+ const root = actx.folder
150
+ const model: any = actx.model
151
+
152
+ // A name, never a path: Path.join NORMALISES a traversal rather than
153
+ // refusing it, so `go/../go` is indistinguishable from `go` once joined.
154
+ // Same grammar as the add side, checked before any path is derived.
155
+ if (!ITEM_NAME_RE.test(name)) {
156
+ throw new SdkGenError(
157
+ 'Invalid ' + kind + ' name: ' + JSON.stringify(name) +
158
+ '\n a name matches ' + ITEM_NAME_RE.source + ' — it is not a path')
159
+ }
160
+
161
+ const declared: any = kindCollection(model, kind)?.[name]
162
+ const modelfile = definitionPath(root, kind, name)
163
+ const hasModel = fs.existsSync(modelfile)
164
+
165
+ if (null == declared && !hasModel) {
166
+ throw new SdkGenError(
167
+ kind + ' not installed: ' + name +
168
+ '\n nothing at ' + rel(root, modelfile) + ', and the model does not declare it')
169
+ }
170
+
171
+ if ('feature' === kind && ('test' === name || BASE_FEATURE === name)) {
172
+ throw new SdkGenError(
173
+ 'feature remove: `' + name + '` cannot be removed' +
174
+ '\n every target\'s generated test suite depends on it, and `target add`' +
175
+ ' installs it unconditionally')
176
+ }
177
+
178
+ const plan: RemovePlan = {
179
+ kind, name, files: [], dirs: [], indexed: false,
180
+ refused: [], aliased: [], notes: [],
181
+ }
182
+
183
+ // Feature overlays belong to both the feature and the target's tree.
184
+ const inScope = (k: string, n: string) =>
185
+ (k === kind && n === name) ||
186
+ ('feature' === kind && 'target' === k) ||
187
+ ('target' === kind && 'feature' === k)
188
+
189
+ const source = resolveDeclared(kind, name, declared, actx)
190
+
191
+ if (null == source) {
192
+ plan.refused.push('(source not found: the copy cannot be compared with what' +
193
+ ' `' + kind + ' add` wrote; every file below is deleted only under --force)')
194
+ }
195
+
196
+ // The feature being removed counts as selected, so its own source is
197
+ // compared rather than written off as stale — see checkTarget.
198
+ const findings = null == source ? { all: [], aliased: new Set<string>() } :
199
+ await driftFindings(actx, inScope, 'feature' === kind ? [name] : undefined)
200
+
201
+ if ('feature' === kind) {
202
+ planFeature(plan, actx)
203
+ }
204
+ else {
205
+ for (const tree of kindTrees(kind, name)) {
206
+ const dir = Path.join(root, ...tree.path.split('/'))
207
+ if (!fs.existsSync(dir)) {
208
+ continue
209
+ }
210
+ plan.dirs.push(tree.path)
211
+ plan.files.push(...walk(fs, dir).map((r) => tree.path + '/' + r))
212
+ }
213
+ }
214
+
215
+ if (hasModel) {
216
+ plan.files.push('model/' + kind + '/' + name + '.aon')
217
+ }
218
+
219
+ const wanted = new Set(plan.files)
220
+ for (const f of findings.all) {
221
+ if (wanted.has(f)) {
222
+ plan.refused.push(f)
223
+ if (findings.aliased.has(f)) {
224
+ plan.aliased.push(f)
225
+ }
226
+ }
227
+ }
228
+
229
+ const index = Path.join(definitionFolder(root, kind), indexName(kind))
230
+ plan.indexed = fs.existsSync(index) &&
231
+ removeIndexEntries(String(fs.readFileSync(index, 'utf8')), [name]) !==
232
+ String(fs.readFileSync(index, 'utf8'))
233
+
234
+ if ('target' === kind) {
235
+ planOutput(plan, actx, declared, deleteOutput)
236
+ }
237
+
238
+ plan.notes.push(...projectMentions(kind, name, actx))
239
+
240
+ if ('feature' === kind && 0 < plan.files.length) {
241
+ plan.notes.push(name + ': a target\'s cross-feature test suite may still' +
242
+ ' name this feature; `target add <t>` re-applies the trim for each target')
243
+ }
244
+
245
+ return plan
246
+ }
247
+
248
+
249
+ function planFeature(plan: RemovePlan, actx: ActionContext) {
250
+ const fs = actx.fs()
251
+ const root = actx.folder
252
+ const targets: any = (actx.model as any)?.main?.[KIT]?.target ?? {}
253
+
254
+ for (const tname of Object.keys(targets).sort()) {
255
+ const tm = Path.join(root, 'tm', tname)
256
+ if (!fs.existsSync(tm)) {
257
+ continue
258
+ }
259
+
260
+ for (const found of findFeatureSources(fs, tm, [plan.name])) {
261
+ const abs = Path.join(tm, found.path)
262
+ const base = 'tm/' + tname + '/' + found.path
263
+ if (found.folder) {
264
+ plan.dirs.push(base)
265
+ plan.files.push(...walk(fs, abs).map((r) => base + '/' + r))
266
+ }
267
+ else {
268
+ plan.files.push(base)
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+
275
+ function planOutput(
276
+ plan: RemovePlan, actx: ActionContext, declared: any, deleteOutput: boolean,
277
+ ) {
278
+ const fs = actx.fs()
279
+ const root = actx.folder
280
+
281
+ if (null != declared?.output?.path) {
282
+ plan.notes.push(plan.name + ': generates out of tree (' +
283
+ declared.output.path + '); that output is not touched')
284
+ return
285
+ }
286
+
287
+ const output = Path.join(root, '..', plan.name)
288
+
289
+ if (!fs.existsSync(output)) {
290
+ return
291
+ }
292
+
293
+ if (deleteOutput) {
294
+ plan.output = output
295
+ return
296
+ }
297
+
298
+ plan.notes.push(plan.name + ': generated output kept at ' +
299
+ rel(root, output) + '; pass --delete-output to remove it')
300
+ }
301
+
302
+
303
+ function resolveDeclared(
304
+ kind: string, name: string, declared: any, actx: ActionContext,
305
+ ): any | undefined {
306
+ const ref = recordedRef(declared, name) || name
307
+ try {
308
+ return resolveSource(
309
+ ref, kind, { folder: actx.folder, fs: actx.fs, log: actx.log, model: actx.model })
310
+ }
311
+ catch (err: any) {
312
+ return undefined
313
+ }
314
+ }
315
+
316
+
317
+ async function driftFindings(
318
+ actx: ActionContext, scope: (kind: string, name: string) => boolean,
319
+ selected?: string[],
320
+ ): Promise<{ all: string[], aliased: Set<string> }> {
321
+ const quiet = quietLog(actx.log)
322
+ const res: any = await doctor({ ...actx, log: quiet }, scope, selected)
323
+ const report = res.report
324
+
325
+ return {
326
+ all: [
327
+ ...report.forked,
328
+ ...report.edited,
329
+ ...report.stale,
330
+ ...report.additive,
331
+ ...report.aliasedDiff,
332
+ ],
333
+ // Kept apart because it needs different advice — see kindRemove.
334
+ aliased: new Set<string>(report.aliasedDiff),
335
+ }
336
+ }
337
+
338
+
339
+ // Model files outside the item's own that still name it: a project's
340
+ // declarations are the project's, so they are reported, never edited.
341
+ function projectMentions(kind: string, name: string, actx: ActionContext): string[] {
342
+ const fs = actx.fs()
343
+ const root = actx.folder
344
+ const modeldir = Path.join(root, 'model')
345
+
346
+ if (!fs.existsSync(modeldir)) {
347
+ return []
348
+ }
349
+
350
+ const own = kind + '/'
351
+ const re = new RegExp(
352
+ '\\b' + kind + ':\\s*[\'"]?' + escapeRe(name) + '[\'"]?\\s*:')
353
+
354
+ const out: string[] = []
355
+ for (const r of walk(fs, modeldir)) {
356
+ if (!r.endsWith('.aon') || r.startsWith(own)) {
357
+ continue
358
+ }
359
+ let src = ''
360
+ try {
361
+ src = String(fs.readFileSync(Path.join(modeldir, r), 'utf8'))
362
+ }
363
+ catch (err: any) {
364
+ continue
365
+ }
366
+ if (re.test(src)) {
367
+ out.push(name + ': model/' + r + ' still declares main.' + KIT + '.' +
368
+ ('edition' === kind ? 'doc.edition' : kind) + '.' + name +
369
+ '; remove that declaration by hand')
370
+ }
371
+ }
372
+
373
+ return out
374
+ }
375
+
376
+
377
+ function applyRemove(plan: RemovePlan, actx: ActionContext, dryrun: boolean): string[] {
378
+ const fs = actx.fs()
379
+ const log = actx.log
380
+ const root = actx.folder
381
+ const { kind, name } = plan
382
+
383
+ const touched: string[] = []
384
+
385
+ const say = (file: string, what: string) => {
386
+ touched.push(file)
387
+ log.info({
388
+ point: 'remove-file', kind, [kind]: name, file, dryrun,
389
+ note: (dryrun ? 'would remove ' : 'removed ') + what
390
+ })
391
+ }
392
+
393
+ for (const f of plan.files) {
394
+ say(f, f)
395
+ if (!dryrun) {
396
+ fs.unlinkSync(Path.join(root, ...f.split('/')))
397
+ }
398
+ }
399
+
400
+ for (const d of plan.dirs) {
401
+ if (!dryrun) {
402
+ rmEmptyTree(fs, Path.join(root, ...d.split('/')))
403
+ }
404
+ }
405
+
406
+ if (plan.indexed) {
407
+ const index = Path.join(definitionFolder(root, kind), indexName(kind))
408
+ say('model/' + kind + '/' + indexName(kind), 'the index entry for ' + name)
409
+ if (!dryrun) {
410
+ fs.writeFileSync(index,
411
+ removeIndexEntries(String(fs.readFileSync(index, 'utf8')), [name]))
412
+ }
413
+ }
414
+
415
+ if (null != plan.output) {
416
+ say(rel(root, plan.output), 'generated output ' + rel(root, plan.output))
417
+ if (!dryrun) {
418
+ fs.rmSync(plan.output, { recursive: true, force: true })
419
+ }
420
+ }
421
+
422
+ if (!dryrun) {
423
+ delete kindCollection(actx.model, kind)[name]
424
+ }
425
+
426
+ return touched
427
+ }
428
+
429
+
430
+ // Drop a tree whose files have gone. Anything still inside (a droppings
431
+ // file the walk skipped, say) goes with it: the tree is the item's.
432
+ function rmEmptyTree(fs: any, dir: string) {
433
+ if (!fs.existsSync(dir)) {
434
+ return
435
+ }
436
+ fs.rmSync(dir, { recursive: true, force: true })
437
+ }
438
+
439
+
440
+ function walk(fs: any, dir: string): string[] {
441
+ const out: string[] = []
442
+
443
+ const descend = (r: string) => {
444
+ const abs = '' === r ? dir : Path.join(dir, r)
445
+ for (const entry of fs.readdirSync(abs).sort()) {
446
+ if (isJunk(entry)) {
447
+ continue
448
+ }
449
+ const er = '' === r ? entry : r + '/' + entry
450
+ if (fs.statSync(Path.join(dir, er)).isDirectory()) {
451
+ descend(er)
452
+ }
453
+ else {
454
+ out.push(er)
455
+ }
456
+ }
457
+ }
458
+
459
+ descend('')
460
+
461
+ return out.sort()
462
+ }
463
+
464
+
465
+ function rel(root: string, abs: string): string {
466
+ return Path.relative(root, abs).split(Path.sep).join('/')
467
+ }
468
+
469
+
470
+ function escapeRe(s: string): string {
471
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
472
+ }
473
+
474
+
475
+ function quietLog(log: any): any {
476
+ const noop = () => { }
477
+ const quiet: any = {
478
+ info: noop, debug: noop, warn: noop, error: noop, trace: noop, fatal: noop,
479
+ }
480
+ quiet.child = () => quiet
481
+ return quiet
482
+ }
483
+
484
+
485
+ export type {
486
+ RemovePlan,
487
+ }
488
+
489
+ export {
490
+ kind_remove,
491
+ planRemove,
492
+ }
@@ -57,7 +57,8 @@ import { BUNDLED, resolveSource, registerInstalled } from './resolve'
57
57
 
58
58
 
59
59
  const CMD_MAP: any = {
60
- add: cmd_target_add
60
+ add: cmd_target_add,
61
+ remove: cmd_target_remove,
61
62
  }
62
63
 
63
64
 
@@ -80,6 +81,12 @@ async function cmd_target_add(args: string[], actx: ActionContext): Promise<Acti
80
81
  }
81
82
 
82
83
 
84
+ // Loaded on use: remove reaches doctor, and doctor reaches this module.
85
+ async function cmd_target_remove(args: string[], actx: ActionContext): Promise<ActionResult> {
86
+ return require('./remove').kind_remove('target', parseAddNames(args), actx)
87
+ }
88
+
89
+
83
90
  // Code API
84
91
  async function target_add(targets: string[], actx: ActionContext): Promise<ActionResult> {
85
92
  const jostraca = actx.jostraca
package/src/cmp/Deploy.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { cmp, each, Content, File } from 'jostraca'
2
2
 
3
+ import { packageVersion } from '../helpers/packageMeta'
4
+
3
5
  import {
4
6
  KIT,
5
7
  getModelPath
@@ -119,7 +121,7 @@ deploy-${t.name}:
119
121
 
120
122
  deploy-dry-${t.name}:
121
123
  \tboru vault exec --dry-run ${ghArgs} -- $(MAKE) tag-push-${t.name}
122
- ${tagPushRecipe(t.name, 'tag-only port')}
124
+ ${tagPushRecipe(t.name, packageVersion(model, t.name), 'tag-only port')}
123
125
  `
124
126
  }
125
127
 
@@ -131,7 +133,7 @@ deploy-${t.name}:
131
133
 
132
134
  deploy-dry-${t.name}:
133
135
  \tboru vault exec --dry-run ${ghArgs} -- $(MAKE) tag-push-${t.name}
134
- ${tagPushRecipe(t.name, reg.name + ' publication pending — tag-only deploy')}
136
+ ${tagPushRecipe(t.name, packageVersion(model, t.name), reg.name + ' publication pending — tag-only deploy')}
135
137
  `
136
138
  }
137
139
 
@@ -175,8 +177,6 @@ SHELL := /bin/bash
175
177
 
176
178
  ${varLines}
177
179
 
178
- # Lockstep SDK version, read from the canonical ts manifest.
179
- VERSION := $(shell node -p "require('./ts/package.json').version" 2>/dev/null || echo 0.0.0)
180
180
  BORU_DRY_RUN_FILLER := BORU-DRY-RUN-FILLER-NOT-A-REAL-SECRET
181
181
 
182
182
  TARGETS := ${targets.map((t: any) => t.name).join(' ')}
@@ -206,10 +206,15 @@ ${deployRules}`
206
206
  // The root-level tag creation + push recipe for a target: boru --dry-run
207
207
  // filler cooperation, idempotent tag creation, token-authenticated https
208
208
  // push (works from an ssh-remote clone without ssh keys).
209
- function tagPushRecipe(name: string, note: string): string {
209
+ function tagPushRecipe(name: string, version: string, note: string): string {
210
+ // The version is embedded in both Make and shell syntax.
211
+ if (!/^[0-9A-Za-z][0-9A-Za-z.+-]*$/.test(version)) {
212
+ throw new Error('Deploy: invalid publish.version for target "' + name + '": ' + JSON.stringify(version))
213
+ }
214
+
210
215
  return `
211
216
  tag-push-${name}:
212
- \t@set -e; tag="${name}/v$(VERSION)"; \\
217
+ \t@set -e; tag="${name}/v${version}"; \\
213
218
  \ttoken="\$\${GITHUB_TOKEN:-$$GH_TOKEN}"; \\
214
219
  \tif [ "$$token" = "$(BORU_DRY_RUN_FILLER)" ]; then \\
215
220
  \t echo "[dry-run] boru filler token detected: would create (if missing) and push tag $$tag; nothing pushed."; exit 0; fi; \\
@@ -114,7 +114,9 @@ function satisfies(version: string, range: string): boolean | undefined {
114
114
  let unknown = false
115
115
 
116
116
  for (const alt of text.split('||')) {
117
- const comparators = alt.trim().split(/\s+/).filter((s) => '' !== s)
117
+ const comparators = alt.trim()
118
+ .replace(/(^|\s)(>=|<=|>|<|=|\^|~)\s+(?=v?\d)/g, '$1$2')
119
+ .split(/\s+/).filter((s) => '' !== s)
118
120
 
119
121
  if (0 === comparators.length) {
120
122
  unknown = true