@effect-app/cli 1.26.4 → 1.26.5
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/CHANGELOG.md +6 -0
- package/dist/gist.d.ts +90 -23
- package/dist/gist.d.ts.map +1 -1
- package/dist/gist.js +217 -97
- package/package.json +3 -3
- package/src/gist.ts +303 -129
- package/test.gists.yaml +15 -7
- package/tsconfig.json +0 -1
package/src/gist.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { RunCommandService } from "./os-command.js"
|
|
|
17
17
|
export class GistEntry extends Schema.Class<GistEntry>("GistEntry")({
|
|
18
18
|
description: Schema.String,
|
|
19
19
|
public: Schema.Boolean,
|
|
20
|
+
company: Schema.String,
|
|
20
21
|
files: Schema.Array(Schema.String)
|
|
21
22
|
}) {}
|
|
22
23
|
|
|
@@ -96,16 +97,26 @@ export class GistEntryDecoded extends GistEntry.transformOrFail<GistEntryDecoded
|
|
|
96
97
|
}) {}
|
|
97
98
|
|
|
98
99
|
export class GistYAML extends Schema.Class<GistYAML>("GistYAML")({
|
|
99
|
-
gists: Schema
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
gists: Schema
|
|
101
|
+
.Record({
|
|
102
|
+
key: Schema.String,
|
|
103
|
+
value: GistEntryDecoded
|
|
104
|
+
})
|
|
105
|
+
.pipe(Schema.optionalWith({
|
|
106
|
+
default: () => ({}),
|
|
107
|
+
nullable: true,
|
|
108
|
+
exact: true
|
|
109
|
+
})),
|
|
103
110
|
settings: Schema.Struct({
|
|
104
111
|
token_env: Schema.String,
|
|
105
112
|
base_directory: Schema.String
|
|
106
113
|
})
|
|
107
114
|
}) {}
|
|
108
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Cache entry representing a gist mapping.
|
|
118
|
+
* Each entry contains the gist's human-readable name and GitHub ID.
|
|
119
|
+
*/
|
|
109
120
|
export class GistCacheEntry extends Schema.Class<GistCacheEntry>("GistCacheEntry")({
|
|
110
121
|
name: Schema.String,
|
|
111
122
|
id: Schema.String
|
|
@@ -115,14 +126,16 @@ export const GistCacheEntries = Schema.Array(GistCacheEntry)
|
|
|
115
126
|
export interface GistCacheEntries extends Schema.Schema.Type<typeof GistCacheEntries> {}
|
|
116
127
|
|
|
117
128
|
/**
|
|
118
|
-
* Gist cache mapping YAML configuration names to GitHub gist IDs.
|
|
129
|
+
* Gist cache mapping YAML configuration names to GitHub gist IDs with company awareness.
|
|
119
130
|
*
|
|
120
131
|
* Since GitHub gists don't have user-defined names, we maintain a cache
|
|
121
132
|
* that maps the human-readable names from our YAML config to actual gist IDs.
|
|
133
|
+
* Each cache entry is associated with a company, enabling multi-tenant operations.
|
|
122
134
|
* This allows us to:
|
|
123
135
|
* - Update existing gists instead of creating duplicates
|
|
124
136
|
* - Clean up obsolete entries when gists are removed from config
|
|
125
137
|
* - Persist the name->ID mapping across CLI runs
|
|
138
|
+
* - Isolate gist operations by company context
|
|
126
139
|
*
|
|
127
140
|
* The cache itself is stored as a secret GitHub gist for persistence.
|
|
128
141
|
*/
|
|
@@ -130,9 +143,12 @@ export class GistCache {
|
|
|
130
143
|
entries: GistCacheEntries
|
|
131
144
|
gist_id: string
|
|
132
145
|
|
|
133
|
-
|
|
146
|
+
company: string
|
|
147
|
+
|
|
148
|
+
constructor({ company, entries, gist_id }: { entries: GistCacheEntries; gist_id: string; company: string }) {
|
|
134
149
|
this.entries = entries
|
|
135
150
|
this.gist_id = gist_id
|
|
151
|
+
this.company = company
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
|
|
@@ -141,11 +157,16 @@ export class GistCache {
|
|
|
141
157
|
// Errors
|
|
142
158
|
//
|
|
143
159
|
class GistCacheNotFound extends Data.TaggedError("GistCacheNotFound")<{
|
|
144
|
-
readonly
|
|
160
|
+
readonly message: string
|
|
161
|
+
}> {}
|
|
162
|
+
|
|
163
|
+
class GistCacheOfCompanyNotFound extends Data.TaggedError("GistCacheOfCompanyNotFound")<{
|
|
164
|
+
readonly message: string
|
|
165
|
+
readonly cache_gist_id: string
|
|
145
166
|
}> {}
|
|
146
167
|
|
|
147
168
|
class GistYAMLError extends Data.TaggedError("GistYAMLError")<{
|
|
148
|
-
readonly
|
|
169
|
+
readonly message: string
|
|
149
170
|
}> {}
|
|
150
171
|
|
|
151
172
|
//
|
|
@@ -153,6 +174,7 @@ class GistYAMLError extends Data.TaggedError("GistYAMLError")<{
|
|
|
153
174
|
// Services
|
|
154
175
|
//
|
|
155
176
|
|
|
177
|
+
// @effect-diagnostics-next-line missingEffectServiceDependency:off
|
|
156
178
|
class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
157
179
|
dependencies: [RunCommandService.Default],
|
|
158
180
|
effect: Effect.gen(function*() {
|
|
@@ -184,9 +206,15 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
184
206
|
return gist_id && gist_id.length > 0 ? Option.some(gist_id) : Option.none()
|
|
185
207
|
}
|
|
186
208
|
|
|
187
|
-
const loadGistCache
|
|
209
|
+
const loadGistCache: (
|
|
210
|
+
company: string,
|
|
211
|
+
rec?: { recCache?: boolean; recCacheCompany?: boolean }
|
|
212
|
+
) => Effect.Effect<GistCache, GistCacheOfCompanyNotFound, never> = Effect
|
|
188
213
|
.fn("effa-cli.gist.loadGistCache")(
|
|
189
|
-
function*(
|
|
214
|
+
function*(
|
|
215
|
+
company: string,
|
|
216
|
+
{ recCache = false, recCacheCompany = false } = { recCache: false, recCacheCompany: false }
|
|
217
|
+
) {
|
|
190
218
|
// search for existing cache gist
|
|
191
219
|
const output = yield* runGetStringSuppressed(`gh gist list --filter "${CACHE_GIST_DESCRIPTION}"`)
|
|
192
220
|
.pipe(Effect.orElse(() => Effect.succeed("")))
|
|
@@ -196,59 +224,81 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
196
224
|
// extract first gist ID (should be our cache gist)
|
|
197
225
|
const firstLine = lines[0]
|
|
198
226
|
if (!firstLine) {
|
|
199
|
-
return yield* new GistCacheNotFound({
|
|
227
|
+
return yield* new GistCacheNotFound({ message: "Empty gist list output" })
|
|
200
228
|
}
|
|
201
229
|
|
|
202
230
|
const parts = firstLine.split(/\s+/)
|
|
203
231
|
const gist_id = parts[0]?.trim()
|
|
204
232
|
|
|
205
233
|
if (!gist_id) {
|
|
206
|
-
|
|
234
|
+
if (recCache) {
|
|
235
|
+
return yield* Effect.dieMessage("Failed to create or locate cache gist after creation attempt")
|
|
236
|
+
}
|
|
237
|
+
return yield* new GistCacheNotFound({ message: "No gist ID found in output" })
|
|
207
238
|
} else {
|
|
208
239
|
yield* Effect.logInfo(`Found existing cache gist with ID ${gist_id}`)
|
|
209
240
|
}
|
|
210
241
|
|
|
211
|
-
// read cache
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
242
|
+
// read company-specific cache file
|
|
243
|
+
const filesInCache = yield* runGetStringSuppressed(`gh gist view ${gist_id} --files`).pipe(
|
|
244
|
+
Effect.map((files) =>
|
|
245
|
+
files
|
|
246
|
+
.trim()
|
|
247
|
+
.split("\n")
|
|
248
|
+
.map((f) => f.trim())
|
|
249
|
+
)
|
|
219
250
|
)
|
|
220
251
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
252
|
+
if (!filesInCache.includes(`${company}.json`)) {
|
|
253
|
+
if (recCacheCompany) {
|
|
254
|
+
return yield* Effect.dieMessage(
|
|
255
|
+
`Failed to create or locate cache entry for company ${company} after creation attempt`
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
return yield* new GistCacheOfCompanyNotFound({
|
|
259
|
+
message: `Cache gist not found of company ${company}`,
|
|
260
|
+
cache_gist_id: gist_id
|
|
261
|
+
})
|
|
262
|
+
} else {
|
|
263
|
+
const cacheContent = yield* runGetStringSuppressed(`gh gist view ${gist_id} -f "${company}.json"`)
|
|
264
|
+
|
|
265
|
+
const entries = yield* pipe(
|
|
266
|
+
cacheContent,
|
|
267
|
+
pipe(Schema.parseJson(GistCacheEntries), Schema.decodeUnknown),
|
|
232
268
|
Effect.orDie
|
|
233
269
|
)
|
|
234
270
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
271
|
+
return new GistCache({ entries, gist_id, company })
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
(_, company) =>
|
|
275
|
+
_.pipe(
|
|
276
|
+
Effect.catchTag("GistCacheNotFound", () =>
|
|
277
|
+
Effect.gen(function*() {
|
|
278
|
+
yield* Effect.logInfo("Cache gist not found, creating new cache...")
|
|
279
|
+
|
|
280
|
+
yield* runGetStringSuppressed(
|
|
281
|
+
`echo "do_not_delete" | gh gist create --desc="${CACHE_GIST_DESCRIPTION}" -f effa-gist.cache -`
|
|
282
|
+
)
|
|
238
283
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
284
|
+
// retry loading the cache after creating it
|
|
285
|
+
return yield* loadGistCache(company, { recCache: true })
|
|
286
|
+
}))
|
|
287
|
+
),
|
|
288
|
+
(_, company) =>
|
|
289
|
+
_.pipe(
|
|
290
|
+
Effect.catchTag("GistCacheOfCompanyNotFound", (e) =>
|
|
291
|
+
Effect.gen(function*() {
|
|
292
|
+
yield* Effect.logInfo(`Cache for company ${company} not found, creating company-specific cache file...`)
|
|
293
|
+
|
|
294
|
+
yield* runGetStringSuppressed(
|
|
295
|
+
`echo "[]" | gh gist edit ${e.cache_gist_id} -a ${company}.json -`
|
|
296
|
+
)
|
|
248
297
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
298
|
+
// retry loading the cache after creating it
|
|
299
|
+
return yield* loadGistCache(company, { recCacheCompany: true })
|
|
300
|
+
}))
|
|
301
|
+
)
|
|
252
302
|
)
|
|
253
303
|
|
|
254
304
|
const saveGistCache = Effect.fn("effa-cli.gist.saveGistCache")(
|
|
@@ -260,17 +310,22 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
260
310
|
Effect.orDie
|
|
261
311
|
)
|
|
262
312
|
|
|
263
|
-
yield* runGetExitCodeSuppressed(
|
|
313
|
+
yield* runGetExitCodeSuppressed(
|
|
314
|
+
`echo '${cacheJson}' | gh gist edit ${cache.gist_id} -f ${cache.company}.json -`
|
|
315
|
+
)
|
|
264
316
|
}
|
|
265
317
|
)
|
|
266
318
|
|
|
267
319
|
const createGistWithFiles = Effect.fn("GHGistService.createGistWithFiles")(
|
|
268
|
-
function*({
|
|
320
|
+
function*({ description, env, files, gist_name, is_public }: {
|
|
269
321
|
gist_name: string
|
|
270
322
|
description: string
|
|
271
|
-
files:
|
|
323
|
+
files: {
|
|
324
|
+
path: string
|
|
325
|
+
name: string
|
|
326
|
+
}[]
|
|
272
327
|
is_public: boolean
|
|
273
|
-
|
|
328
|
+
env: string
|
|
274
329
|
}) {
|
|
275
330
|
yield* Effect.logInfo(`Creating gist ${gist_name} with ${files.length} file(s)`)
|
|
276
331
|
|
|
@@ -280,7 +335,7 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
280
335
|
"create",
|
|
281
336
|
`--desc="${description}"`,
|
|
282
337
|
is_public ? "--public" : "",
|
|
283
|
-
...files.map((
|
|
338
|
+
...files.map((file) => `"${file.path}"`)
|
|
284
339
|
]
|
|
285
340
|
.filter((x) => !!x)
|
|
286
341
|
.join(" ")
|
|
@@ -289,7 +344,7 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
289
344
|
const gistUrl = yield* runGetStringSuppressed(ghCommand)
|
|
290
345
|
|
|
291
346
|
// extract ID from URL
|
|
292
|
-
|
|
347
|
+
const gistNameId = yield* pipe(
|
|
293
348
|
gistUrl,
|
|
294
349
|
extractGistIdFromUrl,
|
|
295
350
|
Option.match({
|
|
@@ -297,57 +352,94 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
297
352
|
onSome: (id) =>
|
|
298
353
|
Effect
|
|
299
354
|
.succeed(
|
|
300
|
-
|
|
301
|
-
gist_id: cache.gist_id,
|
|
302
|
-
entries: [...cache.entries, { name: gist_name, id }]
|
|
303
|
-
})
|
|
355
|
+
{ name: gist_name, id }
|
|
304
356
|
)
|
|
305
357
|
.pipe(Effect.tap(Effect.logInfo(`Created gist with ID ${id}`)))
|
|
306
358
|
})
|
|
307
359
|
)
|
|
360
|
+
|
|
361
|
+
// rename all files to include environment prefix for multi-environment support
|
|
362
|
+
for (const file of files) {
|
|
363
|
+
const originalName = file.name
|
|
364
|
+
const name_with_env = `${env}.${originalName}`
|
|
365
|
+
const ghRenameCommand = [
|
|
366
|
+
"gh",
|
|
367
|
+
"gist",
|
|
368
|
+
"rename",
|
|
369
|
+
gistNameId.id,
|
|
370
|
+
originalName,
|
|
371
|
+
name_with_env
|
|
372
|
+
]
|
|
373
|
+
.join(" ")
|
|
374
|
+
|
|
375
|
+
yield* Effect.logInfo(`Renaming file ${originalName} to ${name_with_env} in gist ${gist_name}`)
|
|
376
|
+
yield* runGetStringSuppressed(ghRenameCommand)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return gistNameId
|
|
308
380
|
}
|
|
309
381
|
)
|
|
310
382
|
|
|
311
383
|
const getGistFileNames = Effect.fn("getGistFileNames")(
|
|
312
|
-
function*({ gist_id, gist_name }: {
|
|
384
|
+
function*({ env, gist_id, gist_name }: {
|
|
313
385
|
gist_id: string
|
|
314
386
|
gist_name: string
|
|
387
|
+
env: string
|
|
315
388
|
}) {
|
|
316
389
|
yield* Effect.logInfo(`Retrieving file names from gist ${gist_name} with ID ${gist_id}`)
|
|
317
390
|
const output = yield* runGetStringSuppressed(`gh gist view ${gist_id} --files`)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
391
|
+
|
|
392
|
+
// filter file names by environment prefix and remove the prefix
|
|
393
|
+
// files in gists are prefixed with "env." to support multiple environments
|
|
394
|
+
return Array.filterMap(
|
|
395
|
+
output
|
|
396
|
+
.trim()
|
|
397
|
+
.split("\n"),
|
|
398
|
+
(fn) => {
|
|
399
|
+
const fnTrimmed = fn.trim()
|
|
400
|
+
if (!fnTrimmed.startsWith(env + ".")) {
|
|
401
|
+
return Option.none()
|
|
402
|
+
}
|
|
403
|
+
return Option.some(
|
|
404
|
+
fnTrimmed.substring(env.length + 1) // remove env prefix and dot
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
)
|
|
322
408
|
}
|
|
323
409
|
)
|
|
324
410
|
|
|
325
411
|
const removeFileFromGist = Effect.fn("removeFileFromGist")(
|
|
326
|
-
function*({ file_name, gist_id, gist_name }: {
|
|
412
|
+
function*({ env, file_name, gist_id, gist_name }: {
|
|
327
413
|
gist_id: string
|
|
328
414
|
gist_name: string
|
|
329
415
|
file_name: string
|
|
416
|
+
env: string
|
|
330
417
|
}) {
|
|
331
|
-
|
|
332
|
-
|
|
418
|
+
const name_with_env = `${env}.${file_name}`
|
|
419
|
+
yield* Effect.logInfo(`Removing file ${name_with_env} from gist ${gist_name}`)
|
|
420
|
+
return yield* runGetExitCodeSuppressed(`gh gist edit ${gist_id} --remove "${name_with_env}"`)
|
|
333
421
|
}
|
|
334
422
|
)
|
|
335
423
|
|
|
336
424
|
const updateFileOfGist = Effect.fn("updateFileOfGist")(
|
|
337
|
-
function*({ file_name, file_path, gist_id, gist_name }: {
|
|
425
|
+
function*({ env, file_name, file_path, gist_id, gist_name }: {
|
|
338
426
|
gist_id: string
|
|
339
427
|
gist_name: string
|
|
340
428
|
file_name: string
|
|
341
429
|
file_path: string
|
|
430
|
+
env: string
|
|
342
431
|
}) {
|
|
343
|
-
|
|
432
|
+
const name_with_env = `${env}.${file_name}`
|
|
433
|
+
yield* Effect.logInfo(`Updating file ${name_with_env} located at ${file_path} of gist ${gist_name}`)
|
|
434
|
+
|
|
435
|
+
// it seems this does not require renaming the file
|
|
344
436
|
const editCommand = [
|
|
345
437
|
"gh",
|
|
346
438
|
"gist",
|
|
347
439
|
"edit",
|
|
348
440
|
gist_id,
|
|
349
441
|
"-f",
|
|
350
|
-
|
|
442
|
+
name_with_env,
|
|
351
443
|
`"${file_path}"`
|
|
352
444
|
]
|
|
353
445
|
.join(" ")
|
|
@@ -357,23 +449,47 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
357
449
|
)
|
|
358
450
|
|
|
359
451
|
const addFileToGist = Effect.fn("addFileToGist")(
|
|
360
|
-
function*({
|
|
452
|
+
function*({ env, file, gist_id, gist_name }: {
|
|
361
453
|
gist_id: string
|
|
362
454
|
gist_name: string
|
|
363
|
-
|
|
455
|
+
file: {
|
|
456
|
+
path: string
|
|
457
|
+
name: string
|
|
458
|
+
}
|
|
459
|
+
env: string
|
|
364
460
|
}) {
|
|
365
|
-
yield* Effect.logInfo(`Adding file ${
|
|
461
|
+
yield* Effect.logInfo(`Adding file ${file.path} to gist ${gist_name}`)
|
|
366
462
|
const editCommand = [
|
|
367
463
|
"gh",
|
|
368
464
|
"gist",
|
|
369
465
|
"edit",
|
|
370
466
|
gist_id,
|
|
371
467
|
"-a",
|
|
372
|
-
`"${
|
|
468
|
+
`"${file.path}"`
|
|
373
469
|
]
|
|
374
470
|
.join(" ")
|
|
375
471
|
|
|
376
|
-
|
|
472
|
+
yield* runGetExitCodeSuppressed(editCommand)
|
|
473
|
+
|
|
474
|
+
const renameCommand = [
|
|
475
|
+
"gh",
|
|
476
|
+
"gist",
|
|
477
|
+
"rename",
|
|
478
|
+
gist_id,
|
|
479
|
+
file.name,
|
|
480
|
+
`${env}.${file.name}`
|
|
481
|
+
]
|
|
482
|
+
.join(" ")
|
|
483
|
+
|
|
484
|
+
yield* Effect.logInfo(`Renaming file ${file.name} to ${env}.${file.name} in gist ${gist_name}`)
|
|
485
|
+
return yield* runGetExitCodeSuppressed(renameCommand)
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
const deleteGist = Effect.fn("deleteGist")(
|
|
490
|
+
function*({ gist_id, gist_name }: { gist_id: string; gist_name: string }) {
|
|
491
|
+
yield* Effect.logInfo(`Deleting gist ${gist_name} with ID ${gist_id}`)
|
|
492
|
+
return yield* runGetExitCodeSuppressed(`gh gist delete ${gist_id}`)
|
|
377
493
|
}
|
|
378
494
|
)
|
|
379
495
|
|
|
@@ -419,55 +535,72 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
419
535
|
saveGistCache,
|
|
420
536
|
|
|
421
537
|
/**
|
|
422
|
-
* Creates a new GitHub gist with the specified files and
|
|
423
|
-
* Generates a GitHub CLI command to create the gist
|
|
538
|
+
* Creates a new GitHub gist with the specified files and renames them with environment prefixes.
|
|
539
|
+
* Generates a GitHub CLI command to create the gist, extracts the resulting gist ID,
|
|
540
|
+
* and renames all files with environment prefixes for multi-environment support.
|
|
424
541
|
*
|
|
425
|
-
* @param
|
|
426
|
-
* @param name - The human-readable name for this gist (used in cache mapping)
|
|
542
|
+
* @param gist_name - The human-readable name for this gist (used in cache mapping)
|
|
427
543
|
* @param description - The description for the GitHub gist
|
|
428
|
-
* @param files - Array of file
|
|
544
|
+
* @param files - Array of file objects with path and name properties to include in the gist
|
|
429
545
|
* @param is_public - Whether the gist should be public or private
|
|
430
|
-
* @
|
|
546
|
+
* @param env - Environment prefix to prepend to file names (e.g., "local-dev", "prod")
|
|
547
|
+
* @returns An Effect that yields a gist entry object with name and id properties
|
|
431
548
|
*/
|
|
432
549
|
createGistWithFiles,
|
|
433
550
|
|
|
434
551
|
/**
|
|
435
|
-
* Retrieves file names from a GitHub gist.
|
|
436
|
-
* Fetches the list of files contained in the specified gist
|
|
552
|
+
* Retrieves file names from a GitHub gist, filtered by environment prefix.
|
|
553
|
+
* Fetches the list of files contained in the specified gist and returns only
|
|
554
|
+
* those that match the current environment, with the environment prefix removed.
|
|
437
555
|
*
|
|
438
556
|
* @param gist_id - The GitHub gist ID to retrieve file names from
|
|
439
557
|
* @param gist_name - The human-readable name of the gist (for logging purposes)
|
|
440
|
-
* @
|
|
558
|
+
* @param env - Environment prefix to filter files by (e.g., "local-dev", "prod")
|
|
559
|
+
* @returns An Effect that yields an array of file names with environment prefix removed
|
|
441
560
|
*/
|
|
442
561
|
getGistFileNames,
|
|
443
562
|
|
|
444
563
|
/**
|
|
445
564
|
* Removes a file from a specified GitHub gist.
|
|
565
|
+
* The file name is automatically prefixed with the environment when removing.
|
|
446
566
|
* @param gist_id - The ID of the gist to modify
|
|
447
567
|
* @param gist_name - The human-readable name of the gist (for logging purposes)
|
|
448
|
-
* @param file_name - The name of the file to remove
|
|
568
|
+
* @param file_name - The base name of the file to remove (without environment prefix)
|
|
569
|
+
* @param env - Environment prefix that was used when the file was added
|
|
449
570
|
* @returns An Effect that succeeds when the file is removed
|
|
450
571
|
*/
|
|
451
572
|
removeFileFromGist,
|
|
452
573
|
|
|
453
574
|
/**
|
|
454
575
|
* Updates a file in a specified GitHub gist.
|
|
576
|
+
* The file name is automatically prefixed with the environment when updating.
|
|
455
577
|
* @param gist_id - The ID of the gist to modify
|
|
456
578
|
* @param gist_name - The human-readable name of the gist (for logging purposes)
|
|
457
|
-
* @param file_name - The name of the file to
|
|
579
|
+
* @param file_name - The base name of the file to update (without environment prefix)
|
|
458
580
|
* @param file_path - The local path of the file to update in the gist
|
|
581
|
+
* @param env - Environment prefix that was used when the file was added
|
|
459
582
|
* @returns An Effect that succeeds when the file is updated
|
|
460
583
|
*/
|
|
461
584
|
updateFileOfGist,
|
|
462
585
|
|
|
463
586
|
/**
|
|
464
587
|
* Adds a new file to a specified GitHub gist.
|
|
588
|
+
* The file is automatically renamed with an environment prefix for multi-environment support.
|
|
465
589
|
* @param gist_id - The ID of the gist to modify
|
|
466
590
|
* @param gist_name - The human-readable name of the gist (for logging purposes)
|
|
467
|
-
* @param
|
|
468
|
-
* @
|
|
591
|
+
* @param file - The file object containing path and name properties
|
|
592
|
+
* @param env - Environment prefix to prepend to the file name
|
|
593
|
+
* @returns An Effect that succeeds when the file is added and renamed
|
|
594
|
+
*/
|
|
595
|
+
addFileToGist,
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Deletes a specified GitHub gist by its ID.
|
|
599
|
+
* @param gist_id - The ID of the gist to delete
|
|
600
|
+
* @param gist_name - The human-readable name of the gist (for logging purposes)
|
|
601
|
+
* @returns An Effect that succeeds when the gist is deleted
|
|
469
602
|
*/
|
|
470
|
-
|
|
603
|
+
deleteGist
|
|
471
604
|
}
|
|
472
605
|
})
|
|
473
606
|
}) {}
|
|
@@ -485,6 +618,14 @@ export class GistHandler extends Effect.Service<GistHandler>()("GistHandler", {
|
|
|
485
618
|
|
|
486
619
|
return {
|
|
487
620
|
handler: Effect.fn("effa-cli.gist.GistHandler")(function*({ YAMLPath }: { YAMLPath: string }) {
|
|
621
|
+
// load company and environment from environment variables
|
|
622
|
+
const CONFIG = yield* Effect.all({
|
|
623
|
+
company: Config.string("COMPANY"),
|
|
624
|
+
env: Config.string("ENV").pipe(Config.withDefault("local-dev"))
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
yield* Effect.logInfo(`Company: ${CONFIG.company}, ENV: ${CONFIG.env}`)
|
|
628
|
+
|
|
488
629
|
yield* Effect.logInfo(`Reading configuration from ${YAMLPath}`)
|
|
489
630
|
|
|
490
631
|
const configExists = yield* fs.exists(YAMLPath)
|
|
@@ -492,14 +633,14 @@ export class GistHandler extends Effect.Service<GistHandler>()("GistHandler", {
|
|
|
492
633
|
return yield* Effect.fail(new Error(`Configuration file not found: ${YAMLPath}`))
|
|
493
634
|
}
|
|
494
635
|
|
|
495
|
-
const
|
|
636
|
+
const configFromYaml = yield* pipe(
|
|
496
637
|
YAMLPath,
|
|
497
638
|
fs.readFileString,
|
|
498
639
|
Effect.andThen((content) =>
|
|
499
640
|
Effect.try({
|
|
500
641
|
try: () => yaml.load(content),
|
|
501
642
|
catch(error) {
|
|
502
|
-
return new GistYAMLError({
|
|
643
|
+
return new GistYAMLError({ message: `Failed to parse YAML: ${(error as Error).message}` })
|
|
503
644
|
}
|
|
504
645
|
})
|
|
505
646
|
),
|
|
@@ -507,17 +648,24 @@ export class GistHandler extends Effect.Service<GistHandler>()("GistHandler", {
|
|
|
507
648
|
)
|
|
508
649
|
|
|
509
650
|
// load GitHub token securely from environment variable
|
|
510
|
-
const redactedToken = yield* Config.redacted(
|
|
651
|
+
const redactedToken = yield* Config.redacted(configFromYaml.settings.token_env)
|
|
511
652
|
|
|
512
|
-
yield* Effect.logInfo(`Using GitHub token from environment variable: ${
|
|
653
|
+
yield* Effect.logInfo(`Using GitHub token from environment variable: ${configFromYaml.settings.token_env}`)
|
|
513
654
|
yield* Effect.logInfo(`Token loaded: ${redactedToken}`) // this will show <redacted> in logs
|
|
514
655
|
|
|
515
656
|
yield* GH.login(Redacted.value(redactedToken))
|
|
516
657
|
|
|
517
|
-
const cache = yield* SynchronizedRef.make<GistCache>(yield* GH.loadGistCache())
|
|
658
|
+
const cache = yield* SynchronizedRef.make<GistCache>(yield* GH.loadGistCache(CONFIG.company))
|
|
659
|
+
|
|
660
|
+
// filter YAML gists by company to ensure isolation between different organizations
|
|
661
|
+
// this prevents cross-company gist operations and maintains data separation
|
|
662
|
+
const thisCompanyGistsFromYaml = Object
|
|
663
|
+
.entries(configFromYaml.gists)
|
|
664
|
+
.filter(([, v]) => v.company === CONFIG.company)
|
|
518
665
|
|
|
519
|
-
|
|
520
|
-
|
|
666
|
+
for (
|
|
667
|
+
const [name, gistConfig] of thisCompanyGistsFromYaml
|
|
668
|
+
) {
|
|
521
669
|
const { description, files_with_name, public: is_public } = gistConfig
|
|
522
670
|
|
|
523
671
|
yield* Effect.logInfo(`Processing gist ${name}`)
|
|
@@ -526,7 +674,7 @@ export class GistHandler extends Effect.Service<GistHandler>()("GistHandler", {
|
|
|
526
674
|
.all(
|
|
527
675
|
files_with_name.map((f) =>
|
|
528
676
|
Effect.gen(function*() {
|
|
529
|
-
const fullPath = path.join(
|
|
677
|
+
const fullPath = path.join(configFromYaml.settings.base_directory, f.path)
|
|
530
678
|
const fileExists = yield* fs.exists(fullPath)
|
|
531
679
|
|
|
532
680
|
if (!fileExists) {
|
|
@@ -557,7 +705,8 @@ export class GistHandler extends Effect.Service<GistHandler>()("GistHandler", {
|
|
|
557
705
|
const gistFileNames = new Set(
|
|
558
706
|
yield* GH.getGistFileNames({
|
|
559
707
|
gist_id: gistFromCache.id,
|
|
560
|
-
gist_name: gistFromCache.name
|
|
708
|
+
gist_name: gistFromCache.name,
|
|
709
|
+
env: CONFIG.env
|
|
561
710
|
})
|
|
562
711
|
)
|
|
563
712
|
|
|
@@ -569,7 +718,8 @@ export class GistHandler extends Effect.Service<GistHandler>()("GistHandler", {
|
|
|
569
718
|
yield* GH.removeFileFromGist({
|
|
570
719
|
gist_id: gistFromCache.id,
|
|
571
720
|
gist_name: gistFromCache.name,
|
|
572
|
-
file_name: gf
|
|
721
|
+
file_name: gf,
|
|
722
|
+
env: CONFIG.env
|
|
573
723
|
})
|
|
574
724
|
}
|
|
575
725
|
}
|
|
@@ -581,60 +731,84 @@ export class GistHandler extends Effect.Service<GistHandler>()("GistHandler", {
|
|
|
581
731
|
gist_id: gistFromCache.id,
|
|
582
732
|
gist_name: gistFromCache.name,
|
|
583
733
|
file_name: f.name,
|
|
584
|
-
file_path: f.path
|
|
734
|
+
file_path: f.path,
|
|
735
|
+
env: CONFIG.env
|
|
585
736
|
})
|
|
586
737
|
} else {
|
|
587
738
|
yield* GH.addFileToGist({
|
|
588
739
|
gist_id: gistFromCache.id,
|
|
589
740
|
gist_name: gistFromCache.name,
|
|
590
|
-
|
|
741
|
+
file: f,
|
|
742
|
+
env: CONFIG.env
|
|
591
743
|
})
|
|
592
744
|
}
|
|
593
745
|
}
|
|
594
746
|
} else {
|
|
595
747
|
if (filesOnDiskWithFullPath.length !== 0) {
|
|
596
|
-
yield* SynchronizedRef.getAndUpdateEffect(
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
748
|
+
yield* SynchronizedRef.getAndUpdateEffect(
|
|
749
|
+
cache,
|
|
750
|
+
Effect.fnUntraced(function*(cache) {
|
|
751
|
+
return new GistCache({
|
|
752
|
+
gist_id: cache.gist_id,
|
|
753
|
+
entries: [
|
|
754
|
+
...cache.entries,
|
|
755
|
+
{
|
|
756
|
+
...(yield* GH.createGistWithFiles({
|
|
757
|
+
gist_name: name,
|
|
758
|
+
description,
|
|
759
|
+
is_public,
|
|
760
|
+
|
|
761
|
+
files: filesOnDiskWithFullPath,
|
|
762
|
+
env: CONFIG.env
|
|
763
|
+
}))
|
|
764
|
+
}
|
|
765
|
+
],
|
|
766
|
+
company: cache.company
|
|
767
|
+
})
|
|
603
768
|
})
|
|
604
|
-
|
|
769
|
+
)
|
|
605
770
|
} else {
|
|
606
771
|
yield* Effect.logWarning(`No valid files found for gist ${name}, skipping creation...`)
|
|
607
772
|
}
|
|
608
773
|
}
|
|
774
|
+
}
|
|
609
775
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
cache,
|
|
617
|
-
Effect.fnUntraced(function*(cache) {
|
|
618
|
-
const newEntries = [...cache.entries]
|
|
776
|
+
// cache cleanup: remove gists that are no longer in YAML configuration
|
|
777
|
+
// only affects entries for the current company to maintain isolation
|
|
778
|
+
const configGistNames = new Set(
|
|
779
|
+
thisCompanyGistsFromYaml
|
|
780
|
+
.map(([name]) => name)
|
|
781
|
+
)
|
|
619
782
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
783
|
+
const newCache = yield* SynchronizedRef.updateAndGetEffect(
|
|
784
|
+
cache,
|
|
785
|
+
Effect.fnUntraced(function*(cache) {
|
|
786
|
+
const newEntries = [...cache.entries]
|
|
787
|
+
|
|
788
|
+
// remove obsolete cache entries for current company only
|
|
789
|
+
// this ensures gists from other companies remain untouched
|
|
790
|
+
for (let i = newEntries.length - 1; i >= 0; i--) {
|
|
791
|
+
const cacheEntry = newEntries[i]
|
|
792
|
+
if (cacheEntry && !configGistNames.has(cacheEntry.name)) {
|
|
793
|
+
// delete the actual gist from GitHub
|
|
794
|
+
yield* GH.deleteGist({
|
|
795
|
+
gist_id: cacheEntry.id,
|
|
796
|
+
gist_name: cacheEntry.name
|
|
797
|
+
})
|
|
798
|
+
yield* Effect.logInfo(
|
|
799
|
+
`Obsolete gist ${cacheEntry.name} of company ${CONFIG.company} with ID ${cacheEntry.id}) will be removed from cache`
|
|
800
|
+
)
|
|
801
|
+
newEntries.splice(i, 1)
|
|
628
802
|
}
|
|
803
|
+
}
|
|
629
804
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
805
|
+
return { ...cache, entries: newEntries }
|
|
806
|
+
})
|
|
807
|
+
)
|
|
633
808
|
|
|
634
|
-
|
|
809
|
+
yield* GH.saveGistCache(newCache)
|
|
635
810
|
|
|
636
|
-
|
|
637
|
-
}
|
|
811
|
+
yield* Effect.logInfo("Gist operations completed")
|
|
638
812
|
})
|
|
639
813
|
}
|
|
640
814
|
})
|