@effect-app/cli 1.26.3 → 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 +12 -0
- package/dist/gist.d.ts +90 -23
- package/dist/gist.d.ts.map +1 -1
- package/dist/gist.js +220 -100
- package/package.json +3 -3
- package/src/gist.ts +309 -135
- 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,18 +174,13 @@ 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*() {
|
|
159
181
|
const CACHE_GIST_DESCRIPTION = "GIST_CACHE_DO_NOT_EDIT_effa_cli_internal"
|
|
160
182
|
const { runGetExitCode, runGetString } = yield* RunCommandService
|
|
161
183
|
|
|
162
|
-
if ((yield* runGetExitCode("gh --version").pipe(Effect.orDie)) !== 0) {
|
|
163
|
-
return yield* Effect.dieMessage(
|
|
164
|
-
"GitHub CLI (gh) is not installed or not found in PATH. Please install it to use the gist command."
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
184
|
// the client cannot recover from PlatformErrors, so we convert failures into defects to clean up the signatures
|
|
169
185
|
const runGetExitCodeSuppressed = (...args: Parameters<typeof runGetExitCode>) => {
|
|
170
186
|
return runGetExitCode(...args).pipe(
|
|
@@ -190,9 +206,15 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
190
206
|
return gist_id && gist_id.length > 0 ? Option.some(gist_id) : Option.none()
|
|
191
207
|
}
|
|
192
208
|
|
|
193
|
-
const loadGistCache
|
|
209
|
+
const loadGistCache: (
|
|
210
|
+
company: string,
|
|
211
|
+
rec?: { recCache?: boolean; recCacheCompany?: boolean }
|
|
212
|
+
) => Effect.Effect<GistCache, GistCacheOfCompanyNotFound, never> = Effect
|
|
194
213
|
.fn("effa-cli.gist.loadGistCache")(
|
|
195
|
-
function*(
|
|
214
|
+
function*(
|
|
215
|
+
company: string,
|
|
216
|
+
{ recCache = false, recCacheCompany = false } = { recCache: false, recCacheCompany: false }
|
|
217
|
+
) {
|
|
196
218
|
// search for existing cache gist
|
|
197
219
|
const output = yield* runGetStringSuppressed(`gh gist list --filter "${CACHE_GIST_DESCRIPTION}"`)
|
|
198
220
|
.pipe(Effect.orElse(() => Effect.succeed("")))
|
|
@@ -202,59 +224,81 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
202
224
|
// extract first gist ID (should be our cache gist)
|
|
203
225
|
const firstLine = lines[0]
|
|
204
226
|
if (!firstLine) {
|
|
205
|
-
return yield* new GistCacheNotFound({
|
|
227
|
+
return yield* new GistCacheNotFound({ message: "Empty gist list output" })
|
|
206
228
|
}
|
|
207
229
|
|
|
208
230
|
const parts = firstLine.split(/\s+/)
|
|
209
231
|
const gist_id = parts[0]?.trim()
|
|
210
232
|
|
|
211
233
|
if (!gist_id) {
|
|
212
|
-
|
|
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" })
|
|
213
238
|
} else {
|
|
214
239
|
yield* Effect.logInfo(`Found existing cache gist with ID ${gist_id}`)
|
|
215
240
|
}
|
|
216
241
|
|
|
217
|
-
// read cache
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
)
|
|
225
250
|
)
|
|
226
251
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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),
|
|
238
268
|
Effect.orDie
|
|
239
269
|
)
|
|
240
270
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
)
|
|
244
283
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
)
|
|
254
297
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
298
|
+
// retry loading the cache after creating it
|
|
299
|
+
return yield* loadGistCache(company, { recCacheCompany: true })
|
|
300
|
+
}))
|
|
301
|
+
)
|
|
258
302
|
)
|
|
259
303
|
|
|
260
304
|
const saveGistCache = Effect.fn("effa-cli.gist.saveGistCache")(
|
|
@@ -266,17 +310,22 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
266
310
|
Effect.orDie
|
|
267
311
|
)
|
|
268
312
|
|
|
269
|
-
yield* runGetExitCodeSuppressed(
|
|
313
|
+
yield* runGetExitCodeSuppressed(
|
|
314
|
+
`echo '${cacheJson}' | gh gist edit ${cache.gist_id} -f ${cache.company}.json -`
|
|
315
|
+
)
|
|
270
316
|
}
|
|
271
317
|
)
|
|
272
318
|
|
|
273
319
|
const createGistWithFiles = Effect.fn("GHGistService.createGistWithFiles")(
|
|
274
|
-
function*({
|
|
320
|
+
function*({ description, env, files, gist_name, is_public }: {
|
|
275
321
|
gist_name: string
|
|
276
322
|
description: string
|
|
277
|
-
files:
|
|
323
|
+
files: {
|
|
324
|
+
path: string
|
|
325
|
+
name: string
|
|
326
|
+
}[]
|
|
278
327
|
is_public: boolean
|
|
279
|
-
|
|
328
|
+
env: string
|
|
280
329
|
}) {
|
|
281
330
|
yield* Effect.logInfo(`Creating gist ${gist_name} with ${files.length} file(s)`)
|
|
282
331
|
|
|
@@ -286,7 +335,7 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
286
335
|
"create",
|
|
287
336
|
`--desc="${description}"`,
|
|
288
337
|
is_public ? "--public" : "",
|
|
289
|
-
...files.map((
|
|
338
|
+
...files.map((file) => `"${file.path}"`)
|
|
290
339
|
]
|
|
291
340
|
.filter((x) => !!x)
|
|
292
341
|
.join(" ")
|
|
@@ -295,7 +344,7 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
295
344
|
const gistUrl = yield* runGetStringSuppressed(ghCommand)
|
|
296
345
|
|
|
297
346
|
// extract ID from URL
|
|
298
|
-
|
|
347
|
+
const gistNameId = yield* pipe(
|
|
299
348
|
gistUrl,
|
|
300
349
|
extractGistIdFromUrl,
|
|
301
350
|
Option.match({
|
|
@@ -303,57 +352,94 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
303
352
|
onSome: (id) =>
|
|
304
353
|
Effect
|
|
305
354
|
.succeed(
|
|
306
|
-
|
|
307
|
-
gist_id: cache.gist_id,
|
|
308
|
-
entries: [...cache.entries, { name: gist_name, id }]
|
|
309
|
-
})
|
|
355
|
+
{ name: gist_name, id }
|
|
310
356
|
)
|
|
311
357
|
.pipe(Effect.tap(Effect.logInfo(`Created gist with ID ${id}`)))
|
|
312
358
|
})
|
|
313
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
|
|
314
380
|
}
|
|
315
381
|
)
|
|
316
382
|
|
|
317
383
|
const getGistFileNames = Effect.fn("getGistFileNames")(
|
|
318
|
-
function*({ gist_id, gist_name }: {
|
|
384
|
+
function*({ env, gist_id, gist_name }: {
|
|
319
385
|
gist_id: string
|
|
320
386
|
gist_name: string
|
|
387
|
+
env: string
|
|
321
388
|
}) {
|
|
322
389
|
yield* Effect.logInfo(`Retrieving file names from gist ${gist_name} with ID ${gist_id}`)
|
|
323
390
|
const output = yield* runGetStringSuppressed(`gh gist view ${gist_id} --files`)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
)
|
|
328
408
|
}
|
|
329
409
|
)
|
|
330
410
|
|
|
331
411
|
const removeFileFromGist = Effect.fn("removeFileFromGist")(
|
|
332
|
-
function*({ file_name, gist_id, gist_name }: {
|
|
412
|
+
function*({ env, file_name, gist_id, gist_name }: {
|
|
333
413
|
gist_id: string
|
|
334
414
|
gist_name: string
|
|
335
415
|
file_name: string
|
|
416
|
+
env: string
|
|
336
417
|
}) {
|
|
337
|
-
|
|
338
|
-
|
|
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}"`)
|
|
339
421
|
}
|
|
340
422
|
)
|
|
341
423
|
|
|
342
424
|
const updateFileOfGist = Effect.fn("updateFileOfGist")(
|
|
343
|
-
function*({ file_name, file_path, gist_id, gist_name }: {
|
|
425
|
+
function*({ env, file_name, file_path, gist_id, gist_name }: {
|
|
344
426
|
gist_id: string
|
|
345
427
|
gist_name: string
|
|
346
428
|
file_name: string
|
|
347
429
|
file_path: string
|
|
430
|
+
env: string
|
|
348
431
|
}) {
|
|
349
|
-
|
|
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
|
|
350
436
|
const editCommand = [
|
|
351
437
|
"gh",
|
|
352
438
|
"gist",
|
|
353
439
|
"edit",
|
|
354
440
|
gist_id,
|
|
355
441
|
"-f",
|
|
356
|
-
|
|
442
|
+
name_with_env,
|
|
357
443
|
`"${file_path}"`
|
|
358
444
|
]
|
|
359
445
|
.join(" ")
|
|
@@ -363,27 +449,57 @@ class GHGistService extends Effect.Service<GHGistService>()("GHGistService", {
|
|
|
363
449
|
)
|
|
364
450
|
|
|
365
451
|
const addFileToGist = Effect.fn("addFileToGist")(
|
|
366
|
-
function*({
|
|
452
|
+
function*({ env, file, gist_id, gist_name }: {
|
|
367
453
|
gist_id: string
|
|
368
454
|
gist_name: string
|
|
369
|
-
|
|
455
|
+
file: {
|
|
456
|
+
path: string
|
|
457
|
+
name: string
|
|
458
|
+
}
|
|
459
|
+
env: string
|
|
370
460
|
}) {
|
|
371
|
-
yield* Effect.logInfo(`Adding file ${
|
|
461
|
+
yield* Effect.logInfo(`Adding file ${file.path} to gist ${gist_name}`)
|
|
372
462
|
const editCommand = [
|
|
373
463
|
"gh",
|
|
374
464
|
"gist",
|
|
375
465
|
"edit",
|
|
376
466
|
gist_id,
|
|
377
467
|
"-a",
|
|
378
|
-
`"${
|
|
468
|
+
`"${file.path}"`
|
|
379
469
|
]
|
|
380
470
|
.join(" ")
|
|
381
471
|
|
|
382
|
-
|
|
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}`)
|
|
383
493
|
}
|
|
384
494
|
)
|
|
385
495
|
|
|
386
496
|
const login = Effect.fn("GHGistService.login")(function*(token: string) {
|
|
497
|
+
if ((yield* runGetExitCode("gh --version").pipe(Effect.orDie)) !== 0) {
|
|
498
|
+
return yield* Effect.dieMessage(
|
|
499
|
+
"GitHub CLI (gh) is not installed or not found in PATH. Please install it to use the gist command."
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
387
503
|
const isLogged = yield* runGetExitCode(`echo ${token} | gh auth login --with-token`).pipe(Effect.orDie)
|
|
388
504
|
if (isLogged !== 0) {
|
|
389
505
|
return yield* Effect.fail(new Error("Failed to log in to GitHub CLI with provided token"))
|
|
@@ -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
|
})
|