@hlix/cli 0.2.0 → 0.3.0

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/dist/index.js CHANGED
@@ -1,4701 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- __commonJS,
4
- __toESM
5
- } from "./chunk-FYS2JH42.js";
6
-
7
- // ../../node_modules/.bun/ignore@7.0.5/node_modules/ignore/index.js
8
- var require_ignore = __commonJS({
9
- "../../node_modules/.bun/ignore@7.0.5/node_modules/ignore/index.js"(exports, module) {
10
- "use strict";
11
- function makeArray(subject) {
12
- return Array.isArray(subject) ? subject : [subject];
13
- }
14
- var UNDEFINED = void 0;
15
- var EMPTY = "";
16
- var SPACE = " ";
17
- var ESCAPE = "\\";
18
- var REGEX_TEST_BLANK_LINE = /^\s+$/;
19
- var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
20
- var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
21
- var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
22
- var REGEX_SPLITALL_CRLF = /\r?\n/g;
23
- var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
24
- var REGEX_TEST_TRAILING_SLASH = /\/$/;
25
- var SLASH = "/";
26
- var TMP_KEY_IGNORE = "node-ignore";
27
- if (typeof Symbol !== "undefined") {
28
- TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
29
- }
30
- var KEY_IGNORE = TMP_KEY_IGNORE;
31
- var define = (object, key, value) => {
32
- Object.defineProperty(object, key, { value });
33
- return value;
34
- };
35
- var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
36
- var RETURN_FALSE = () => false;
37
- var sanitizeRange = (range) => range.replace(
38
- REGEX_REGEXP_RANGE,
39
- (match2, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match2 : EMPTY
40
- );
41
- var cleanRangeBackSlash = (slashes) => {
42
- const { length } = slashes;
43
- return slashes.slice(0, length - length % 2);
44
- };
45
- var REPLACERS = [
46
- [
47
- // Remove BOM
48
- // TODO:
49
- // Other similar zero-width characters?
50
- /^\uFEFF/,
51
- () => EMPTY
52
- ],
53
- // > Trailing spaces are ignored unless they are quoted with backslash ("\")
54
- [
55
- // (a\ ) -> (a )
56
- // (a ) -> (a)
57
- // (a ) -> (a)
58
- // (a \ ) -> (a )
59
- /((?:\\\\)*?)(\\?\s+)$/,
60
- (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)
61
- ],
62
- // Replace (\ ) with ' '
63
- // (\ ) -> ' '
64
- // (\\ ) -> '\\ '
65
- // (\\\ ) -> '\\ '
66
- [
67
- /(\\+?)\s/g,
68
- (_, m1) => {
69
- const { length } = m1;
70
- return m1.slice(0, length - length % 2) + SPACE;
71
- }
72
- ],
73
- // Escape metacharacters
74
- // which is written down by users but means special for regular expressions.
75
- // > There are 12 characters with special meanings:
76
- // > - the backslash \,
77
- // > - the caret ^,
78
- // > - the dollar sign $,
79
- // > - the period or dot .,
80
- // > - the vertical bar or pipe symbol |,
81
- // > - the question mark ?,
82
- // > - the asterisk or star *,
83
- // > - the plus sign +,
84
- // > - the opening parenthesis (,
85
- // > - the closing parenthesis ),
86
- // > - and the opening square bracket [,
87
- // > - the opening curly brace {,
88
- // > These special characters are often called "metacharacters".
89
- [
90
- /[\\$.|*+(){^]/g,
91
- (match2) => `\\${match2}`
92
- ],
93
- [
94
- // > a question mark (?) matches a single character
95
- /(?!\\)\?/g,
96
- () => "[^/]"
97
- ],
98
- // leading slash
99
- [
100
- // > A leading slash matches the beginning of the pathname.
101
- // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
102
- // A leading slash matches the beginning of the pathname
103
- /^\//,
104
- () => "^"
105
- ],
106
- // replace special metacharacter slash after the leading slash
107
- [
108
- /\//g,
109
- () => "\\/"
110
- ],
111
- [
112
- // > A leading "**" followed by a slash means match in all directories.
113
- // > For example, "**/foo" matches file or directory "foo" anywhere,
114
- // > the same as pattern "foo".
115
- // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
116
- // > under directory "foo".
117
- // Notice that the '*'s have been replaced as '\\*'
118
- /^\^*\\\*\\\*\\\//,
119
- // '**/foo' <-> 'foo'
120
- () => "^(?:.*\\/)?"
121
- ],
122
- // starting
123
- [
124
- // there will be no leading '/'
125
- // (which has been replaced by section "leading slash")
126
- // If starts with '**', adding a '^' to the regular expression also works
127
- /^(?=[^^])/,
128
- function startingReplacer() {
129
- return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
130
- }
131
- ],
132
- // two globstars
133
- [
134
- // Use lookahead assertions so that we could match more than one `'/**'`
135
- /\\\/\\\*\\\*(?=\\\/|$)/g,
136
- // Zero, one or several directories
137
- // should not use '*', or it will be replaced by the next replacer
138
- // Check if it is not the last `'/**'`
139
- (_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
140
- ],
141
- // normal intermediate wildcards
142
- [
143
- // Never replace escaped '*'
144
- // ignore rule '\*' will match the path '*'
145
- // 'abc.*/' -> go
146
- // 'abc.*' -> skip this rule,
147
- // coz trailing single wildcard will be handed by [trailing wildcard]
148
- /(^|[^\\]+)(\\\*)+(?=.+)/g,
149
- // '*.js' matches '.js'
150
- // '*.js' doesn't match 'abc'
151
- (_, p1, p2) => {
152
- const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
153
- return p1 + unescaped;
154
- }
155
- ],
156
- [
157
- // unescape, revert step 3 except for back slash
158
- // For example, if a user escape a '\\*',
159
- // after step 3, the result will be '\\\\\\*'
160
- /\\\\\\(?=[$.|*+(){^])/g,
161
- () => ESCAPE
162
- ],
163
- [
164
- // '\\\\' -> '\\'
165
- /\\\\/g,
166
- () => ESCAPE
167
- ],
168
- [
169
- // > The range notation, e.g. [a-zA-Z],
170
- // > can be used to match one of the characters in a range.
171
- // `\` is escaped by step 3
172
- /(\\)?\[([^\]/]*?)(\\*)($|\])/g,
173
- (match2, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"
174
- ],
175
- // ending
176
- [
177
- // 'js' will not match 'js.'
178
- // 'ab' will not match 'abc'
179
- /(?:[^*])$/,
180
- // WTF!
181
- // https://git-scm.com/docs/gitignore
182
- // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
183
- // which re-fixes #24, #38
184
- // > If there is a separator at the end of the pattern then the pattern
185
- // > will only match directories, otherwise the pattern can match both
186
- // > files and directories.
187
- // 'js*' will not match 'a.js'
188
- // 'js/' will not match 'a.js'
189
- // 'js' will match 'a.js' and 'a.js/'
190
- (match2) => /\/$/.test(match2) ? `${match2}$` : `${match2}(?=$|\\/$)`
191
- ]
192
- ];
193
- var REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
194
- var MODE_IGNORE = "regex";
195
- var MODE_CHECK_IGNORE = "checkRegex";
196
- var UNDERSCORE = "_";
197
- var TRAILING_WILD_CARD_REPLACERS = {
198
- [MODE_IGNORE](_, p1) {
199
- const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
200
- return `${prefix}(?=$|\\/$)`;
201
- },
202
- [MODE_CHECK_IGNORE](_, p1) {
203
- const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
204
- return `${prefix}(?=$|\\/$)`;
205
- }
206
- };
207
- var makeRegexPrefix = (pattern) => REPLACERS.reduce(
208
- (prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)),
209
- pattern
210
- );
211
- var isString = (subject) => typeof subject === "string";
212
- var checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
213
- var splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
214
- var IgnoreRule = class {
215
- constructor(pattern, mark, body, ignoreCase, negative, prefix) {
216
- this.pattern = pattern;
217
- this.mark = mark;
218
- this.negative = negative;
219
- define(this, "body", body);
220
- define(this, "ignoreCase", ignoreCase);
221
- define(this, "regexPrefix", prefix);
222
- }
223
- get regex() {
224
- const key = UNDERSCORE + MODE_IGNORE;
225
- if (this[key]) {
226
- return this[key];
227
- }
228
- return this._make(MODE_IGNORE, key);
229
- }
230
- get checkRegex() {
231
- const key = UNDERSCORE + MODE_CHECK_IGNORE;
232
- if (this[key]) {
233
- return this[key];
234
- }
235
- return this._make(MODE_CHECK_IGNORE, key);
236
- }
237
- _make(mode, key) {
238
- const str = this.regexPrefix.replace(
239
- REGEX_REPLACE_TRAILING_WILDCARD,
240
- // It does not need to bind pattern
241
- TRAILING_WILD_CARD_REPLACERS[mode]
242
- );
243
- const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
244
- return define(this, key, regex);
245
- }
246
- };
247
- var createRule = ({
248
- pattern,
249
- mark
250
- }, ignoreCase) => {
251
- let negative = false;
252
- let body = pattern;
253
- if (body.indexOf("!") === 0) {
254
- negative = true;
255
- body = body.substr(1);
256
- }
257
- body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
258
- const regexPrefix = makeRegexPrefix(body);
259
- return new IgnoreRule(
260
- pattern,
261
- mark,
262
- body,
263
- ignoreCase,
264
- negative,
265
- regexPrefix
266
- );
267
- };
268
- var RuleManager = class {
269
- constructor(ignoreCase) {
270
- this._ignoreCase = ignoreCase;
271
- this._rules = [];
272
- }
273
- _add(pattern) {
274
- if (pattern && pattern[KEY_IGNORE]) {
275
- this._rules = this._rules.concat(pattern._rules._rules);
276
- this._added = true;
277
- return;
278
- }
279
- if (isString(pattern)) {
280
- pattern = {
281
- pattern
282
- };
283
- }
284
- if (checkPattern(pattern.pattern)) {
285
- const rule = createRule(pattern, this._ignoreCase);
286
- this._added = true;
287
- this._rules.push(rule);
288
- }
289
- }
290
- // @param {Array<string> | string | Ignore} pattern
291
- add(pattern) {
292
- this._added = false;
293
- makeArray(
294
- isString(pattern) ? splitPattern(pattern) : pattern
295
- ).forEach(this._add, this);
296
- return this._added;
297
- }
298
- // Test one single path without recursively checking parent directories
299
- //
300
- // - checkUnignored `boolean` whether should check if the path is unignored,
301
- // setting `checkUnignored` to `false` could reduce additional
302
- // path matching.
303
- // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
304
- // @returns {TestResult} true if a file is ignored
305
- test(path, checkUnignored, mode) {
306
- let ignored = false;
307
- let unignored = false;
308
- let matchedRule;
309
- this._rules.forEach((rule) => {
310
- const { negative } = rule;
311
- if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
312
- return;
313
- }
314
- const matched = rule[mode].test(path);
315
- if (!matched) {
316
- return;
317
- }
318
- ignored = !negative;
319
- unignored = negative;
320
- matchedRule = negative ? UNDEFINED : rule;
321
- });
322
- const ret = {
323
- ignored,
324
- unignored
325
- };
326
- if (matchedRule) {
327
- ret.rule = matchedRule;
328
- }
329
- return ret;
330
- }
331
- };
332
- var throwError = (message, Ctor) => {
333
- throw new Ctor(message);
334
- };
335
- var checkPath = (path, originalPath, doThrow) => {
336
- if (!isString(path)) {
337
- return doThrow(
338
- `path must be a string, but got \`${originalPath}\``,
339
- TypeError
340
- );
341
- }
342
- if (!path) {
343
- return doThrow(`path must not be empty`, TypeError);
344
- }
345
- if (checkPath.isNotRelative(path)) {
346
- const r = "`path.relative()`d";
347
- return doThrow(
348
- `path should be a ${r} string, but got "${originalPath}"`,
349
- RangeError
350
- );
351
- }
352
- return true;
353
- };
354
- var isNotRelative = (path) => REGEX_TEST_INVALID_PATH.test(path);
355
- checkPath.isNotRelative = isNotRelative;
356
- checkPath.convert = (p) => p;
357
- var Ignore = class {
358
- constructor({
359
- ignorecase = true,
360
- ignoreCase = ignorecase,
361
- allowRelativePaths = false
362
- } = {}) {
363
- define(this, KEY_IGNORE, true);
364
- this._rules = new RuleManager(ignoreCase);
365
- this._strictPathCheck = !allowRelativePaths;
366
- this._initCache();
367
- }
368
- _initCache() {
369
- this._ignoreCache = /* @__PURE__ */ Object.create(null);
370
- this._testCache = /* @__PURE__ */ Object.create(null);
371
- }
372
- add(pattern) {
373
- if (this._rules.add(pattern)) {
374
- this._initCache();
375
- }
376
- return this;
377
- }
378
- // legacy
379
- addPattern(pattern) {
380
- return this.add(pattern);
381
- }
382
- // @returns {TestResult}
383
- _test(originalPath, cache, checkUnignored, slices) {
384
- const path = originalPath && checkPath.convert(originalPath);
385
- checkPath(
386
- path,
387
- originalPath,
388
- this._strictPathCheck ? throwError : RETURN_FALSE
389
- );
390
- return this._t(path, cache, checkUnignored, slices);
391
- }
392
- checkIgnore(path) {
393
- if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
394
- return this.test(path);
395
- }
396
- const slices = path.split(SLASH).filter(Boolean);
397
- slices.pop();
398
- if (slices.length) {
399
- const parent = this._t(
400
- slices.join(SLASH) + SLASH,
401
- this._testCache,
402
- true,
403
- slices
404
- );
405
- if (parent.ignored) {
406
- return parent;
407
- }
408
- }
409
- return this._rules.test(path, false, MODE_CHECK_IGNORE);
410
- }
411
- _t(path, cache, checkUnignored, slices) {
412
- if (path in cache) {
413
- return cache[path];
414
- }
415
- if (!slices) {
416
- slices = path.split(SLASH).filter(Boolean);
417
- }
418
- slices.pop();
419
- if (!slices.length) {
420
- return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE);
421
- }
422
- const parent = this._t(
423
- slices.join(SLASH) + SLASH,
424
- cache,
425
- checkUnignored,
426
- slices
427
- );
428
- return cache[path] = parent.ignored ? parent : this._rules.test(path, checkUnignored, MODE_IGNORE);
429
- }
430
- ignores(path) {
431
- return this._test(path, this._ignoreCache, false).ignored;
432
- }
433
- createFilter() {
434
- return (path) => !this.ignores(path);
435
- }
436
- filter(paths) {
437
- return makeArray(paths).filter(this.createFilter());
438
- }
439
- // @returns {TestResult}
440
- test(path) {
441
- return this._test(path, this._testCache, true);
442
- }
443
- };
444
- var factory = (options) => new Ignore(options);
445
- var isPathValid = (path) => checkPath(path && checkPath.convert(path), path, RETURN_FALSE);
446
- var setupWindows = () => {
447
- const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
448
- checkPath.convert = makePosix;
449
- const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
450
- checkPath.isNotRelative = (path) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path) || isNotRelative(path);
451
- };
452
- if (
453
- // Detect `process` so that it can run in browsers.
454
- typeof process !== "undefined" && process.platform === "win32"
455
- ) {
456
- setupWindows();
457
- }
458
- module.exports = factory;
459
- factory.default = factory;
460
- module.exports.isPathValid = isPathValid;
461
- define(module.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
462
- }
463
- });
464
-
465
- // src/cli.ts
466
- import { resolve as resolve3 } from "path";
467
- import { parseArgs } from "util";
468
-
469
- // ../../node_modules/.bun/openapi-fetch@0.17.0/node_modules/openapi-fetch/dist/index.mjs
470
- var PATH_PARAM_RE = /\{[^{}]+\}/g;
471
- var supportsRequestInitExt = () => {
472
- return typeof process === "object" && Number.parseInt(process?.versions?.node?.substring(0, 2)) >= 18 && process.versions.undici;
473
- };
474
- function randomID() {
475
- return Math.random().toString(36).slice(2, 11);
476
- }
477
- function createClient(clientOptions) {
478
- let {
479
- baseUrl = "",
480
- Request: CustomRequest = globalThis.Request,
481
- fetch: baseFetch = globalThis.fetch,
482
- querySerializer: globalQuerySerializer,
483
- bodySerializer: globalBodySerializer,
484
- pathSerializer: globalPathSerializer,
485
- headers: baseHeaders,
486
- requestInitExt = void 0,
487
- ...baseOptions
488
- } = { ...clientOptions };
489
- requestInitExt = supportsRequestInitExt() ? requestInitExt : void 0;
490
- baseUrl = removeTrailingSlash(baseUrl);
491
- const globalMiddlewares = [];
492
- async function coreFetch(schemaPath, fetchOptions) {
493
- const {
494
- baseUrl: localBaseUrl,
495
- fetch = baseFetch,
496
- Request: Request2 = CustomRequest,
497
- headers,
498
- params = {},
499
- parseAs = "json",
500
- querySerializer: requestQuerySerializer,
501
- bodySerializer = globalBodySerializer ?? defaultBodySerializer,
502
- pathSerializer: requestPathSerializer,
503
- body,
504
- middleware: requestMiddlewares = [],
505
- ...init
506
- } = fetchOptions || {};
507
- let finalBaseUrl = baseUrl;
508
- if (localBaseUrl) {
509
- finalBaseUrl = removeTrailingSlash(localBaseUrl) ?? baseUrl;
510
- }
511
- let querySerializer = typeof globalQuerySerializer === "function" ? globalQuerySerializer : createQuerySerializer(globalQuerySerializer);
512
- if (requestQuerySerializer) {
513
- querySerializer = typeof requestQuerySerializer === "function" ? requestQuerySerializer : createQuerySerializer({
514
- ...typeof globalQuerySerializer === "object" ? globalQuerySerializer : {},
515
- ...requestQuerySerializer
516
- });
517
- }
518
- const pathSerializer = requestPathSerializer || globalPathSerializer || defaultPathSerializer;
519
- const serializedBody = body === void 0 ? void 0 : bodySerializer(
520
- body,
521
- // Note: we declare mergeHeaders() both here and below because it’s a bit of a chicken-or-egg situation:
522
- // bodySerializer() needs all headers so we aren’t dropping ones set by the user, however,
523
- // the result of this ALSO sets the lowest-priority content-type header. So we re-merge below,
524
- // setting the content-type at the very beginning to be overwritten.
525
- // Lastly, based on the way headers work, it’s not a simple “present-or-not” check becauase null intentionally un-sets headers.
526
- mergeHeaders(baseHeaders, headers, params.header)
527
- );
528
- const finalHeaders = mergeHeaders(
529
- // with no body, we should not to set Content-Type
530
- serializedBody === void 0 || // if serialized body is FormData; browser will correctly set Content-Type & boundary expression
531
- serializedBody instanceof FormData ? {} : {
532
- "Content-Type": "application/json"
533
- },
534
- baseHeaders,
535
- headers,
536
- params.header
537
- );
538
- const finalMiddlewares = [...globalMiddlewares, ...requestMiddlewares];
539
- const requestInit = {
540
- redirect: "follow",
541
- ...baseOptions,
542
- ...init,
543
- body: serializedBody,
544
- headers: finalHeaders
545
- };
546
- let id;
547
- let options;
548
- let request = new Request2(
549
- createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer, pathSerializer }),
550
- requestInit
551
- );
552
- let response;
553
- for (const key in init) {
554
- if (!(key in request)) {
555
- request[key] = init[key];
556
- }
557
- }
558
- if (finalMiddlewares.length) {
559
- id = randomID();
560
- options = Object.freeze({
561
- baseUrl: finalBaseUrl,
562
- fetch,
563
- parseAs,
564
- querySerializer,
565
- bodySerializer,
566
- pathSerializer
567
- });
568
- for (const m of finalMiddlewares) {
569
- if (m && typeof m === "object" && typeof m.onRequest === "function") {
570
- const result = await m.onRequest({
571
- request,
572
- schemaPath,
573
- params,
574
- options,
575
- id
576
- });
577
- if (result) {
578
- if (result instanceof Request2) {
579
- request = result;
580
- } else if (result instanceof Response) {
581
- response = result;
582
- break;
583
- } else {
584
- throw new Error("onRequest: must return new Request() or Response() when modifying the request");
585
- }
586
- }
587
- }
588
- }
589
- }
590
- if (!response) {
591
- try {
592
- response = await fetch(request, requestInitExt);
593
- } catch (error2) {
594
- let errorAfterMiddleware = error2;
595
- if (finalMiddlewares.length) {
596
- for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
597
- const m = finalMiddlewares[i];
598
- if (m && typeof m === "object" && typeof m.onError === "function") {
599
- const result = await m.onError({
600
- request,
601
- error: errorAfterMiddleware,
602
- schemaPath,
603
- params,
604
- options,
605
- id
606
- });
607
- if (result) {
608
- if (result instanceof Response) {
609
- errorAfterMiddleware = void 0;
610
- response = result;
611
- break;
612
- }
613
- if (result instanceof Error) {
614
- errorAfterMiddleware = result;
615
- continue;
616
- }
617
- throw new Error("onError: must return new Response() or instance of Error");
618
- }
619
- }
620
- }
621
- }
622
- if (errorAfterMiddleware) {
623
- throw errorAfterMiddleware;
624
- }
625
- }
626
- if (finalMiddlewares.length) {
627
- for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
628
- const m = finalMiddlewares[i];
629
- if (m && typeof m === "object" && typeof m.onResponse === "function") {
630
- const result = await m.onResponse({
631
- request,
632
- response,
633
- schemaPath,
634
- params,
635
- options,
636
- id
637
- });
638
- if (result) {
639
- if (!(result instanceof Response)) {
640
- throw new Error("onResponse: must return new Response() when modifying the response");
641
- }
642
- response = result;
643
- }
644
- }
645
- }
646
- }
647
- }
648
- const contentLength = response.headers.get("Content-Length");
649
- if (response.status === 204 || request.method === "HEAD" || contentLength === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked")) {
650
- return response.ok ? { data: void 0, response } : { error: void 0, response };
651
- }
652
- if (response.ok) {
653
- const getResponseData = async () => {
654
- if (parseAs === "stream") {
655
- return response.body;
656
- }
657
- if (parseAs === "json" && !contentLength) {
658
- const raw = await response.text();
659
- return raw ? JSON.parse(raw) : void 0;
660
- }
661
- return await response[parseAs]();
662
- };
663
- return { data: await getResponseData(), response };
664
- }
665
- let error = await response.text();
666
- try {
667
- error = JSON.parse(error);
668
- } catch {
669
- }
670
- return { error, response };
671
- }
672
- return {
673
- request(method, url, init) {
674
- return coreFetch(url, { ...init, method: method.toUpperCase() });
675
- },
676
- /** Call a GET endpoint */
677
- GET(url, init) {
678
- return coreFetch(url, { ...init, method: "GET" });
679
- },
680
- /** Call a PUT endpoint */
681
- PUT(url, init) {
682
- return coreFetch(url, { ...init, method: "PUT" });
683
- },
684
- /** Call a POST endpoint */
685
- POST(url, init) {
686
- return coreFetch(url, { ...init, method: "POST" });
687
- },
688
- /** Call a DELETE endpoint */
689
- DELETE(url, init) {
690
- return coreFetch(url, { ...init, method: "DELETE" });
691
- },
692
- /** Call a OPTIONS endpoint */
693
- OPTIONS(url, init) {
694
- return coreFetch(url, { ...init, method: "OPTIONS" });
695
- },
696
- /** Call a HEAD endpoint */
697
- HEAD(url, init) {
698
- return coreFetch(url, { ...init, method: "HEAD" });
699
- },
700
- /** Call a PATCH endpoint */
701
- PATCH(url, init) {
702
- return coreFetch(url, { ...init, method: "PATCH" });
703
- },
704
- /** Call a TRACE endpoint */
705
- TRACE(url, init) {
706
- return coreFetch(url, { ...init, method: "TRACE" });
707
- },
708
- /** Register middleware */
709
- use(...middleware) {
710
- for (const m of middleware) {
711
- if (!m) {
712
- continue;
713
- }
714
- if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m || "onError" in m)) {
715
- throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`");
716
- }
717
- globalMiddlewares.push(m);
718
- }
719
- },
720
- /** Unregister middleware */
721
- eject(...middleware) {
722
- for (const m of middleware) {
723
- const i = globalMiddlewares.indexOf(m);
724
- if (i !== -1) {
725
- globalMiddlewares.splice(i, 1);
726
- }
727
- }
728
- }
729
- };
730
- }
731
- function serializePrimitiveParam(name, value, options) {
732
- if (value === void 0 || value === null) {
733
- return "";
734
- }
735
- if (typeof value === "object") {
736
- throw new Error(
737
- "Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these."
738
- );
739
- }
740
- return `${name}=${options?.allowReserved === true ? value : encodeURIComponent(value)}`;
741
- }
742
- function serializeObjectParam(name, value, options) {
743
- if (!value || typeof value !== "object") {
744
- return "";
745
- }
746
- const values = [];
747
- const joiner = {
748
- simple: ",",
749
- label: ".",
750
- matrix: ";"
751
- }[options.style] || "&";
752
- if (options.style !== "deepObject" && options.explode === false) {
753
- for (const k in value) {
754
- values.push(k, options.allowReserved === true ? value[k] : encodeURIComponent(value[k]));
755
- }
756
- const final2 = values.join(",");
757
- switch (options.style) {
758
- case "form": {
759
- return `${name}=${final2}`;
760
- }
761
- case "label": {
762
- return `.${final2}`;
763
- }
764
- case "matrix": {
765
- return `;${name}=${final2}`;
766
- }
767
- default: {
768
- return final2;
769
- }
770
- }
771
- }
772
- for (const k in value) {
773
- const finalName = options.style === "deepObject" ? `${name}[${k}]` : k;
774
- values.push(serializePrimitiveParam(finalName, value[k], options));
775
- }
776
- const final = values.join(joiner);
777
- return options.style === "label" || options.style === "matrix" ? `${joiner}${final}` : final;
778
- }
779
- function serializeArrayParam(name, value, options) {
780
- if (!Array.isArray(value)) {
781
- return "";
782
- }
783
- if (options.explode === false) {
784
- const joiner2 = { form: ",", spaceDelimited: "%20", pipeDelimited: "|" }[options.style] || ",";
785
- const final = (options.allowReserved === true ? value : value.map((v) => encodeURIComponent(v))).join(joiner2);
786
- switch (options.style) {
787
- case "simple": {
788
- return final;
789
- }
790
- case "label": {
791
- return `.${final}`;
792
- }
793
- case "matrix": {
794
- return `;${name}=${final}`;
795
- }
796
- // case "spaceDelimited":
797
- // case "pipeDelimited":
798
- default: {
799
- return `${name}=${final}`;
800
- }
801
- }
802
- }
803
- const joiner = { simple: ",", label: ".", matrix: ";" }[options.style] || "&";
804
- const values = [];
805
- for (const v of value) {
806
- if (options.style === "simple" || options.style === "label") {
807
- values.push(options.allowReserved === true ? v : encodeURIComponent(v));
808
- } else {
809
- values.push(serializePrimitiveParam(name, v, options));
810
- }
811
- }
812
- return options.style === "label" || options.style === "matrix" ? `${joiner}${values.join(joiner)}` : values.join(joiner);
813
- }
814
- function createQuerySerializer(options) {
815
- return function querySerializer(queryParams) {
816
- const search = [];
817
- if (queryParams && typeof queryParams === "object") {
818
- for (const name in queryParams) {
819
- const value = queryParams[name];
820
- if (value === void 0 || value === null) {
821
- continue;
822
- }
823
- if (Array.isArray(value)) {
824
- if (value.length === 0) {
825
- continue;
826
- }
827
- search.push(
828
- serializeArrayParam(name, value, {
829
- style: "form",
830
- explode: true,
831
- ...options?.array,
832
- allowReserved: options?.allowReserved || false
833
- })
834
- );
835
- continue;
836
- }
837
- if (typeof value === "object") {
838
- search.push(
839
- serializeObjectParam(name, value, {
840
- style: "deepObject",
841
- explode: true,
842
- ...options?.object,
843
- allowReserved: options?.allowReserved || false
844
- })
845
- );
846
- continue;
847
- }
848
- search.push(serializePrimitiveParam(name, value, options));
849
- }
850
- }
851
- return search.join("&");
852
- };
853
- }
854
- function defaultPathSerializer(pathname, pathParams) {
855
- let nextURL = pathname;
856
- for (const match2 of pathname.match(PATH_PARAM_RE) ?? []) {
857
- let name = match2.substring(1, match2.length - 1);
858
- let explode = false;
859
- let style = "simple";
860
- if (name.endsWith("*")) {
861
- explode = true;
862
- name = name.substring(0, name.length - 1);
863
- }
864
- if (name.startsWith(".")) {
865
- style = "label";
866
- name = name.substring(1);
867
- } else if (name.startsWith(";")) {
868
- style = "matrix";
869
- name = name.substring(1);
870
- }
871
- if (!pathParams || pathParams[name] === void 0 || pathParams[name] === null) {
872
- continue;
873
- }
874
- const value = pathParams[name];
875
- if (Array.isArray(value)) {
876
- nextURL = nextURL.replace(match2, serializeArrayParam(name, value, { style, explode }));
877
- continue;
878
- }
879
- if (typeof value === "object") {
880
- nextURL = nextURL.replace(match2, serializeObjectParam(name, value, { style, explode }));
881
- continue;
882
- }
883
- if (style === "matrix") {
884
- nextURL = nextURL.replace(match2, `;${serializePrimitiveParam(name, value)}`);
885
- continue;
886
- }
887
- nextURL = nextURL.replace(match2, style === "label" ? `.${encodeURIComponent(value)}` : encodeURIComponent(value));
888
- }
889
- return nextURL;
890
- }
891
- function defaultBodySerializer(body, headers) {
892
- if (body instanceof FormData) {
893
- return body;
894
- }
895
- if (headers) {
896
- const contentType = headers.get instanceof Function ? headers.get("Content-Type") ?? headers.get("content-type") : headers["Content-Type"] ?? headers["content-type"];
897
- if (contentType === "application/x-www-form-urlencoded") {
898
- return new URLSearchParams(body).toString();
899
- }
900
- }
901
- return JSON.stringify(body);
902
- }
903
- function createFinalURL(pathname, options) {
904
- let finalURL = `${options.baseUrl}${pathname}`;
905
- if (options.params?.path) {
906
- finalURL = options.pathSerializer(finalURL, options.params.path);
907
- }
908
- let search = options.querySerializer(options.params.query ?? {});
909
- if (search.startsWith("?")) {
910
- search = search.substring(1);
911
- }
912
- if (search) {
913
- finalURL += `?${search}`;
914
- }
915
- return finalURL;
916
- }
917
- function mergeHeaders(...allHeaders) {
918
- const finalHeaders = new Headers();
919
- for (const h of allHeaders) {
920
- if (!h || typeof h !== "object") {
921
- continue;
922
- }
923
- const iterator = h instanceof Headers ? h.entries() : Object.entries(h);
924
- for (const [k, v] of iterator) {
925
- if (v === null) {
926
- finalHeaders.delete(k);
927
- } else if (Array.isArray(v)) {
928
- for (const v2 of v) {
929
- finalHeaders.append(k, v2);
930
- }
931
- } else if (v !== void 0) {
932
- finalHeaders.set(k, v);
933
- }
934
- }
935
- }
936
- return finalHeaders;
937
- }
938
- function removeTrailingSlash(url) {
939
- if (url.endsWith("/")) {
940
- return url.substring(0, url.length - 1);
941
- }
942
- return url;
943
- }
944
-
945
- // ../api-client/src/client.ts
946
- function createHlixClient(options) {
947
- return createClient({
948
- baseUrl: options.baseUrl,
949
- headers: options.headers,
950
- fetch: options.fetch
951
- });
952
- }
953
-
954
- // ../sdk/src/auth.ts
955
- var apiKeyCredential = (apiKey) => ({
956
- kind: "apiKey",
957
- apiKey
958
- });
959
- function credentialHeaders(credential) {
960
- return credential.kind === "apiKey" ? { "x-api-key": credential.apiKey } : { cookie: credential.cookie };
961
- }
962
- function organizationHeaders(organizationId) {
963
- return organizationId ? { "X-Organization-Id": organizationId } : {};
964
- }
965
- function authHeaders(credential, organizationId) {
966
- return {
967
- ...credentialHeaders(credential),
968
- ...organizationHeaders(organizationId)
969
- };
970
- }
971
-
972
- // ../sdk/src/errors.ts
973
- function errorMessageOf(body) {
974
- if (typeof body !== "object" || body === null) return null;
975
- const { error } = body;
976
- return typeof error === "string" ? error : null;
977
- }
978
- var HlixError = class extends Error {
979
- status;
980
- body;
981
- requestId;
982
- method;
983
- url;
984
- constructor(message, init) {
985
- super(message);
986
- this.name = new.target.name;
987
- this.status = init.status;
988
- this.body = init.body;
989
- this.requestId = init.requestId ?? null;
990
- this.method = init.method;
991
- this.url = init.url;
992
- }
993
- };
994
- var HlixValidationError = class extends HlixError {
995
- /** The decoded `ZodIssue[]`; empty when the server sent a plain message. */
996
- issues;
997
- constructor(message, init, issues) {
998
- super(message, init);
999
- this.issues = issues;
1000
- }
1001
- };
1002
- var HlixBadRequestError = class extends HlixError {
1003
- };
1004
- var HlixAuthenticationError = class extends HlixError {
1005
- };
1006
- var HlixPermissionError = class extends HlixError {
1007
- };
1008
- var HlixNotFoundError = class extends HlixError {
1009
- };
1010
- var HlixConflictError = class extends HlixError {
1011
- };
1012
- var HlixRateLimitError = class extends HlixError {
1013
- retryAfter;
1014
- constructor(message, init, retryAfter) {
1015
- super(message, init);
1016
- this.retryAfter = retryAfter;
1017
- }
1018
- };
1019
- var HlixNotImplementedError = class extends HlixError {
1020
- };
1021
- var HlixServerError = class extends HlixError {
1022
- };
1023
- var HlixTransportError = class extends Error {
1024
- method;
1025
- url;
1026
- constructor(message, init) {
1027
- super(message, { cause: init.cause });
1028
- this.name = "HlixTransportError";
1029
- this.method = init.method;
1030
- this.url = init.url;
1031
- }
1032
- };
1033
- function validationIssues(body) {
1034
- if (typeof body !== "object" || body === null) return null;
1035
- const candidate = body;
1036
- if (candidate.success !== false) return null;
1037
- const raw = candidate.error?.message;
1038
- if (typeof raw !== "string") return null;
1039
- try {
1040
- const parsed = JSON.parse(raw);
1041
- return Array.isArray(parsed) ? parsed : [];
1042
- } catch {
1043
- return [];
1044
- }
1045
- }
1046
- function retryAfterSeconds(headers) {
1047
- const raw = headers.get("retry-after");
1048
- if (!raw) return null;
1049
- const seconds = Number(raw);
1050
- return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
1051
- }
1052
- function errorForResponse(response, body, request) {
1053
- const init = {
1054
- status: response.status,
1055
- body,
1056
- requestId: response.headers.get("x-request-id"),
1057
- method: request.method,
1058
- url: request.url
1059
- };
1060
- const stated = errorMessageOf(body);
1061
- const fallback = `${request.method} ${request.url} failed with ${response.status}`;
1062
- const issues = validationIssues(body);
1063
- if (issues) {
1064
- return new HlixValidationError(
1065
- `${request.method} ${request.url}: request body failed validation`,
1066
- init,
1067
- issues
1068
- );
1069
- }
1070
- const message = stated ?? fallback;
1071
- switch (response.status) {
1072
- case 400:
1073
- return new HlixBadRequestError(message, init);
1074
- case 401:
1075
- return new HlixAuthenticationError(message, init);
1076
- case 403:
1077
- return new HlixPermissionError(message, init);
1078
- case 404:
1079
- return new HlixNotFoundError(message, init);
1080
- case 409:
1081
- return new HlixConflictError(message, init);
1082
- case 429:
1083
- return new HlixRateLimitError(message, init, retryAfterSeconds(response.headers));
1084
- case 501:
1085
- return new HlixNotImplementedError(message, init);
1086
- default:
1087
- return response.status >= 500 ? new HlixServerError(message, init) : new HlixError(message, init);
1088
- }
1089
- }
1090
-
1091
- // ../sdk/src/retry.ts
1092
- var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
1093
- "GET",
1094
- "HEAD",
1095
- "PUT",
1096
- "DELETE",
1097
- "OPTIONS",
1098
- "TRACE"
1099
- ]);
1100
- function isIdempotent(method) {
1101
- return IDEMPOTENT_METHODS.has(method.toUpperCase());
1102
- }
1103
- var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
1104
- 429,
1105
- 500,
1106
- 502,
1107
- 503,
1108
- 504
1109
- ]);
1110
- function isRetryableStatus(status) {
1111
- return RETRYABLE_STATUSES.has(status);
1112
- }
1113
- var DEFAULT_RETRY = {
1114
- attempts: 3,
1115
- baseDelayMs: 200,
1116
- maxDelayMs: 5e3
1117
- };
1118
- function backoffDelayMs(attempt, policy, retryAfterSeconds3, random = Math.random) {
1119
- if (retryAfterSeconds3 !== null) {
1120
- return Math.min(retryAfterSeconds3 * 1e3, policy.maxDelayMs);
1121
- }
1122
- const ceiling = Math.min(policy.baseDelayMs * 2 ** attempt, policy.maxDelayMs);
1123
- return Math.round(random() * ceiling);
1124
- }
1125
- function shouldRetry(input) {
1126
- if (!isIdempotent(input.method)) return false;
1127
- if (input.attempt + 1 >= input.policy.attempts) return false;
1128
- return input.status === null || isRetryableStatus(input.status);
1129
- }
1130
-
1131
- // ../sdk/src/http.ts
1132
- var wait = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
1133
- function retryAfterSeconds2(response) {
1134
- const raw = response.headers.get("retry-after");
1135
- if (!raw) return null;
1136
- const seconds = Number(raw);
1137
- return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
1138
- }
1139
- function retryingFetch(options) {
1140
- const { base, policy } = options;
1141
- const sleep = options.sleep ?? wait;
1142
- const random = options.random ?? Math.random;
1143
- return async (input) => {
1144
- const method = input.method.toUpperCase();
1145
- const replayable = isIdempotent(method) ? input.clone() : null;
1146
- for (let attempt = 0; ; attempt += 1) {
1147
- const request = attempt === 0 ? input : replayable.clone();
1148
- let response;
1149
- try {
1150
- response = await base(request);
1151
- } catch (cause) {
1152
- if (!replayable || !shouldRetry({ method, attempt, policy, status: null })) {
1153
- throw new HlixTransportError(
1154
- `${method} ${input.url}: ${cause instanceof Error ? cause.message : String(cause)}`,
1155
- { method, url: input.url, cause }
1156
- );
1157
- }
1158
- await sleep(backoffDelayMs(attempt, policy, null, random));
1159
- continue;
1160
- }
1161
- if (!shouldRetry({ method, attempt, policy, status: response.status })) {
1162
- return response;
1163
- }
1164
- if (!replayable) return response;
1165
- await sleep(backoffDelayMs(attempt, policy, retryAfterSeconds2(response), random));
1166
- }
1167
- };
1168
- }
1169
- function unwrap(result, request) {
1170
- if (result.response.ok) return result.data;
1171
- throw errorForResponse(result.response, result.error ?? result.data, request);
1172
- }
1173
-
1174
- // ../sdk/src/streaming.ts
1175
- var emptyFrame = () => ({ event: null, data: [], id: null, retry: null });
1176
- function applyField(frame, name, value) {
1177
- switch (name) {
1178
- case "event":
1179
- frame.event = value;
1180
- return;
1181
- case "data":
1182
- frame.data.push(value);
1183
- return;
1184
- case "id":
1185
- if (!value.includes("\0")) frame.id = value;
1186
- return;
1187
- case "retry": {
1188
- if (/^\d+$/.test(value)) frame.retry = Number(value);
1189
- return;
1190
- }
1191
- default:
1192
- return;
1193
- }
1194
- }
1195
- function dispatch(frame) {
1196
- const data = frame.data.join("\n");
1197
- const event = frame.event;
1198
- const id = frame.id;
1199
- frame.data = [];
1200
- frame.event = null;
1201
- if (data === "") return null;
1202
- return { event, data, id, retry: frame.retry };
1203
- }
1204
- function consumeLine(frame, line) {
1205
- if (line === "") return dispatch(frame);
1206
- if (line.startsWith(":")) return null;
1207
- const colon = line.indexOf(":");
1208
- if (colon === -1) {
1209
- applyField(frame, line, "");
1210
- return null;
1211
- }
1212
- const name = line.slice(0, colon);
1213
- let value = line.slice(colon + 1);
1214
- if (value.startsWith(" ")) value = value.slice(1);
1215
- applyField(frame, name, value);
1216
- return null;
1217
- }
1218
- function splitLines(buffer) {
1219
- const lines = [];
1220
- let start = 0;
1221
- for (let i = 0; i < buffer.length; i += 1) {
1222
- const ch = buffer[i];
1223
- if (ch !== "\n" && ch !== "\r") continue;
1224
- lines.push(buffer.slice(start, i));
1225
- if (ch === "\r" && buffer[i + 1] === "\n") i += 1;
1226
- start = i + 1;
1227
- }
1228
- return { lines, rest: buffer.slice(start) };
1229
- }
1230
- async function* parseSseStream(body, signal) {
1231
- const reader = body.getReader();
1232
- const decoder = new TextDecoder();
1233
- const frame = emptyFrame();
1234
- let buffer = "";
1235
- const abort = () => void reader.cancel().catch(() => {
1236
- });
1237
- signal?.addEventListener("abort", abort, { once: true });
1238
- try {
1239
- for (; ; ) {
1240
- const { done, value } = await reader.read();
1241
- if (done) break;
1242
- buffer += decoder.decode(value, { stream: true });
1243
- const { lines, rest } = splitLines(buffer);
1244
- buffer = rest;
1245
- for (const line of lines) {
1246
- const event = consumeLine(frame, line);
1247
- if (event) yield event;
1248
- }
1249
- if (signal?.aborted) return;
1250
- }
1251
- buffer += decoder.decode();
1252
- if (buffer !== "") {
1253
- const event = consumeLine(frame, buffer);
1254
- if (event) yield event;
1255
- }
1256
- const trailing = dispatch(frame);
1257
- if (trailing) yield trailing;
1258
- } finally {
1259
- signal?.removeEventListener("abort", abort);
1260
- reader.releaseLock();
1261
- }
1262
- }
1263
-
1264
- // ../sdk/src/index.ts
1265
- var Hlix = class {
1266
- /** The un-opinionated transport, for anything this facade does not wrap. */
1267
- raw;
1268
- baseUrl;
1269
- headers;
1270
- fetcher;
1271
- constructor(options) {
1272
- this.baseUrl = options.baseUrl.replace(/\/+$/, "");
1273
- this.headers = authHeaders(options.credential, options.organizationId);
1274
- const policy = { ...DEFAULT_RETRY, ...options.retry };
1275
- const base = options.fetch ?? ((input) => globalThis.fetch(input));
1276
- this.fetcher = retryingFetch({ base, policy });
1277
- this.raw = createHlixClient({
1278
- baseUrl: this.baseUrl,
1279
- headers: this.headers,
1280
- fetch: this.fetcher
1281
- });
1282
- }
1283
- at(method, path) {
1284
- return { method, url: `${this.baseUrl}${path}` };
1285
- }
1286
- projects = {
1287
- list: async () => unwrap(
1288
- await this.raw.GET("/v1/api/projects"),
1289
- this.at("GET", "/v1/api/projects")
1290
- ),
1291
- get: async (id) => unwrap(
1292
- await this.raw.GET("/v1/api/projects/{id}", { params: { path: { id } } }),
1293
- this.at("GET", `/v1/api/projects/${id}`)
1294
- ),
1295
- create: async (body) => unwrap(
1296
- await this.raw.POST("/v1/api/projects", { body }),
1297
- this.at("POST", "/v1/api/projects")
1298
- ),
1299
- delete: async (id) => unwrap(
1300
- await this.raw.DELETE("/v1/api/projects/{id}", {
1301
- params: { path: { id } }
1302
- }),
1303
- this.at("DELETE", `/v1/api/projects/${id}`)
1304
- ),
1305
- start: async (id, body) => unwrap(
1306
- await this.raw.POST("/v1/api/projects/{id}/start", {
1307
- params: { path: { id } },
1308
- body
1309
- }),
1310
- this.at("POST", `/v1/api/projects/${id}/start`)
1311
- )
1312
- };
1313
- imports = {
1314
- create: async (body) => {
1315
- const result = await this.raw.POST("/v1/api/imports", { body });
1316
- return unwrap(result, this.at("POST", "/v1/api/imports"));
1317
- },
1318
- get: async (id) => unwrap(
1319
- await this.raw.GET("/v1/api/imports/{id}", {
1320
- params: { path: { id } }
1321
- }),
1322
- this.at("GET", `/v1/api/imports/${id}`)
1323
- ),
1324
- finalize: async (id, body) => {
1325
- const result = await this.raw.POST("/v1/api/imports/{id}/finalize", {
1326
- params: { path: { id } },
1327
- body
1328
- });
1329
- return unwrap(
1330
- result,
1331
- this.at("POST", `/v1/api/imports/${id}/finalize`)
1332
- );
1333
- },
1334
- uploadBundle: async (upload, bundle, options = {}) => {
1335
- const chunkSize = options.chunkSize ?? 8 * 1024 * 1024;
1336
- if (chunkSize <= 0 || chunkSize % (256 * 1024) !== 0) {
1337
- throw new Error("Import upload chunkSize must be a positive multiple of 256 KiB");
1338
- }
1339
- const url = upload.url.startsWith("/") ? `${this.baseUrl}${upload.url}` : upload.url;
1340
- if (upload.kind === "hlix-stream") {
1341
- const request = new Request(url, {
1342
- method: "PUT",
1343
- headers: {
1344
- ...this.headers,
1345
- "content-type": "application/x-git-bundle",
1346
- "content-length": String(bundle.size)
1347
- },
1348
- body: bundle,
1349
- signal: options.signal
1350
- });
1351
- const response = await this.fetcher(request);
1352
- if (!response.ok) await this.throwUploadError(response, url);
1353
- options.onProgress?.(bundle.size, bundle.size);
1354
- return;
1355
- }
1356
- let offset = 0;
1357
- while (offset < bundle.size) {
1358
- const end = Math.min(offset + chunkSize, bundle.size);
1359
- const request = new Request(url, {
1360
- method: "PUT",
1361
- headers: {
1362
- "content-type": "application/x-git-bundle",
1363
- "content-length": String(end - offset),
1364
- "content-range": `bytes ${offset}-${end - 1}/${bundle.size}`
1365
- },
1366
- body: bundle.slice(offset, end),
1367
- signal: options.signal
1368
- });
1369
- const response = await this.fetcher(request);
1370
- if (response.status !== 308 && !response.ok) {
1371
- await this.throwUploadError(response, url);
1372
- }
1373
- const acknowledged = response.headers.get("range")?.match(/bytes=0-(\d+)/)?.[1];
1374
- offset = acknowledged ? Number(acknowledged) + 1 : end;
1375
- if (!Number.isSafeInteger(offset) || offset < end || offset > bundle.size) {
1376
- throw new Error("Import upload server returned an invalid acknowledged range");
1377
- }
1378
- options.onProgress?.(offset, bundle.size);
1379
- }
1380
- }
1381
- };
1382
- revisions = {
1383
- head: async (projectId) => unwrap(
1384
- await this.raw.GET("/v1/api/projects/{id}/revisions/head", {
1385
- params: { path: { id: projectId } }
1386
- }),
1387
- this.at("GET", `/v1/api/projects/${projectId}/revisions/head`)
1388
- ),
1389
- protectedFiles: async (projectId) => unwrap(
1390
- await this.raw.GET("/v1/api/projects/{id}/protected-files", {
1391
- params: { path: { id: projectId } }
1392
- }),
1393
- this.at("GET", `/v1/api/projects/${projectId}/protected-files`)
1394
- ),
1395
- createUpload: async (projectId, body) => {
1396
- const result = await this.raw.POST("/v1/api/projects/{id}/revisions/uploads", {
1397
- params: { path: { id: projectId } },
1398
- body
1399
- });
1400
- return unwrap(result, this.at("POST", `/v1/api/projects/${projectId}/revisions/uploads`));
1401
- },
1402
- finalize: async (projectId, uploadId, body) => {
1403
- const result = await this.raw.POST(
1404
- "/v1/api/projects/{id}/revisions/uploads/{uploadId}/finalize",
1405
- { params: { path: { id: projectId, uploadId } }, body }
1406
- );
1407
- return unwrap(
1408
- result,
1409
- this.at("POST", `/v1/api/projects/${projectId}/revisions/uploads/${uploadId}/finalize`)
1410
- );
1411
- },
1412
- uploadBundle: async (upload, bundle, options = {}) => this.imports.uploadBundle(upload, bundle, options),
1413
- downloadBundle: async (projectId, revisionId) => {
1414
- const path = `/v1/api/projects/${projectId}/revisions/${revisionId}/bundle`;
1415
- const request = new Request(`${this.baseUrl}${path}`, {
1416
- method: "GET",
1417
- headers: this.headers
1418
- });
1419
- const response = await this.fetcher(request);
1420
- if (!response.ok) await this.throwUploadError(response, `${this.baseUrl}${path}`);
1421
- return response.blob();
1422
- }
1423
- };
1424
- resources = {
1425
- importSkill: async (projectId, body) => unwrap(
1426
- await this.raw.POST("/v1/api/projects/{id}/resources/skill", {
1427
- params: { path: { id: projectId } },
1428
- body
1429
- }),
1430
- this.at("POST", `/v1/api/projects/${projectId}/resources/skill`)
1431
- ),
1432
- importAgent: async (projectId, body) => unwrap(
1433
- await this.raw.POST("/v1/api/projects/{id}/resources/agent", {
1434
- params: { path: { id: projectId } },
1435
- body
1436
- }),
1437
- this.at("POST", `/v1/api/projects/${projectId}/resources/agent`)
1438
- ),
1439
- importMcp: async (projectId, body) => unwrap(
1440
- await this.raw.POST("/v1/api/projects/{id}/resources/mcp", {
1441
- params: { path: { id: projectId } },
1442
- body
1443
- }),
1444
- this.at("POST", `/v1/api/projects/${projectId}/resources/mcp`)
1445
- )
1446
- };
1447
- async throwUploadError(response, url) {
1448
- let body = null;
1449
- try {
1450
- body = await response.json();
1451
- } catch {
1452
- body = await response.text().catch(() => null);
1453
- }
1454
- throw errorForResponse(response, body, { method: "PUT", url });
1455
- }
1456
- tasks = {
1457
- list: async (query) => unwrap(
1458
- await this.raw.GET("/v1/api/tasks", { params: { query: query ?? {} } }),
1459
- this.at("GET", "/v1/api/tasks")
1460
- ),
1461
- get: async (id) => unwrap(
1462
- await this.raw.GET("/v1/api/tasks/{id}", { params: { path: { id } } }),
1463
- this.at("GET", `/v1/api/tasks/${id}`)
1464
- ),
1465
- create: async (body) => unwrap(
1466
- await this.raw.POST("/v1/api/tasks", { body }),
1467
- this.at("POST", "/v1/api/tasks")
1468
- ),
1469
- update: async (id, body) => unwrap(
1470
- await this.raw.PATCH("/v1/api/tasks/{id}", {
1471
- params: { path: { id } },
1472
- body
1473
- }),
1474
- this.at("PATCH", `/v1/api/tasks/${id}`)
1475
- ),
1476
- listComments: async (id) => unwrap(
1477
- await this.raw.GET("/v1/api/tasks/{id}/comments", {
1478
- params: { path: { id } }
1479
- }),
1480
- this.at("GET", `/v1/api/tasks/${id}/comments`)
1481
- ),
1482
- addComment: async (id, body) => unwrap(
1483
- await this.raw.POST("/v1/api/tasks/{id}/comments", {
1484
- params: { path: { id } },
1485
- body
1486
- }),
1487
- this.at("POST", `/v1/api/tasks/${id}/comments`)
1488
- ),
1489
- /** The verified review evidence — the same observation QA reviews. */
1490
- review: async (id) => unwrap(
1491
- await this.raw.GET("/v1/api/tasks/{id}/review", {
1492
- params: { path: { id } }
1493
- }),
1494
- this.at("GET", `/v1/api/tasks/${id}/review`)
1495
- ),
1496
- /** Live status frames. A terminal status closes the stream server-side. */
1497
- stream: (id, init) => this.stream(`/v1/api/tasks/${id}/stream`, init)
1498
- };
1499
- cycles = {
1500
- list: async (query) => unwrap(
1501
- await this.raw.GET("/v1/api/cycles", {
1502
- params: { query: query ?? {} }
1503
- }),
1504
- this.at("GET", "/v1/api/cycles")
1505
- ),
1506
- get: async (id) => unwrap(
1507
- await this.raw.GET("/v1/api/cycles/{id}", {
1508
- params: { path: { id } }
1509
- }),
1510
- this.at("GET", `/v1/api/cycles/${id}`)
1511
- ),
1512
- /** Creation is a PROPOSAL — the orchestrator owns the roadmap. */
1513
- propose: async (body) => unwrap(
1514
- await this.raw.POST("/v1/api/cycles", { body }),
1515
- this.at("POST", "/v1/api/cycles")
1516
- ),
1517
- stream: (id, init) => this.stream(`/v1/api/cycles/${id}/stream`, init)
1518
- };
1519
- /**
1520
- * Invoices the agency issues to its own customer.
1521
- *
1522
- * NOT the hlix subscription — that is Polar's, and the two never meet.
1523
- *
1524
- * There is no `update`, because there is no route that could edit an issued
1525
- * invoice: a correction is `void` plus a new draft naming it through
1526
- * `replacesInvoiceId`. And `create` names no customer — the customer is
1527
- * derived from the billables being invoiced.
1528
- */
1529
- clientInvoices = {
1530
- list: async (query) => unwrap(
1531
- await this.raw.GET("/v1/api/client-billing/invoices", {
1532
- params: { query: query ?? {} }
1533
- }),
1534
- this.at("GET", "/v1/api/client-billing/invoices")
1535
- ),
1536
- get: async (id) => unwrap(
1537
- await this.raw.GET("/v1/api/client-billing/invoices/{id}", {
1538
- params: { path: { id } }
1539
- }),
1540
- this.at("GET", `/v1/api/client-billing/invoices/${id}`)
1541
- ),
1542
- createDraft: async (body) => unwrap(
1543
- await this.raw.POST("/v1/api/client-billing/invoices", { body }),
1544
- this.at("POST", "/v1/api/client-billing/invoices")
1545
- ),
1546
- /** The number is assigned by the server, gapless per workspace. */
1547
- issue: async (id) => unwrap(
1548
- await this.raw.POST("/v1/api/client-billing/invoices/{id}/issue", {
1549
- params: { path: { id } },
1550
- body: {}
1551
- }),
1552
- this.at("POST", `/v1/api/client-billing/invoices/${id}/issue`)
1553
- ),
1554
- void: async (id, reason) => unwrap(
1555
- await this.raw.POST("/v1/api/client-billing/invoices/{id}/void", {
1556
- params: { path: { id } },
1557
- body: { reason }
1558
- }),
1559
- this.at("POST", `/v1/api/client-billing/invoices/${id}/void`)
1560
- ),
1561
- pay: async (id) => unwrap(
1562
- await this.raw.POST("/v1/api/client-billing/invoices/{id}/pay", {
1563
- params: { path: { id } },
1564
- body: {}
1565
- }),
1566
- this.at("POST", `/v1/api/client-billing/invoices/${id}/pay`)
1567
- ),
1568
- /**
1569
- * Open a PROVIDER-HOSTED checkout. Returns a URL and nothing else — no card
1570
- * field and no payment credential ever passes through hlix or this client.
1571
- * It does not mark the invoice paid: that happens only when the provider's
1572
- * signed webhook reconciles.
1573
- */
1574
- paymentLink: async (id, body = {}) => unwrap(
1575
- await this.raw.POST(
1576
- "/v1/api/client-billing/invoices/{id}/payment-link",
1577
- { params: { path: { id } }, body }
1578
- ),
1579
- this.at("POST", `/v1/api/client-billing/invoices/${id}/payment-link`)
1580
- )
1581
- };
1582
- /**
1583
- * Consume any `text/event-stream` route.
1584
- *
1585
- * Deliberately NOT routed through the typed client: `openapi-fetch` parses
1586
- * the body, and a stream must not be parsed — it has to be read as it
1587
- * arrives. Retry does not apply either; a stream that drops mid-flight has
1588
- * already delivered events, and silently restarting it would replay them.
1589
- */
1590
- async *stream(path, init) {
1591
- const url = `${this.baseUrl}${path}`;
1592
- const request = new Request(url, {
1593
- headers: { ...this.headers, accept: "text/event-stream" },
1594
- signal: init?.signal
1595
- });
1596
- const response = await globalThis.fetch(request);
1597
- if (!response.ok || !response.body) {
1598
- let body = null;
1599
- try {
1600
- body = await response.json();
1601
- } catch {
1602
- body = null;
1603
- }
1604
- throw errorForResponse(response, body, { method: "GET", url });
1605
- }
1606
- yield* parseSseStream(response.body, init?.signal);
1607
- }
1608
- };
1609
-
1610
- // src/errors.ts
1611
- var CliError = class extends Error {
1612
- code;
1613
- constructor(code, message) {
1614
- super(message);
1615
- this.name = "CliError";
1616
- this.code = code;
1617
- }
1618
- };
1619
-
1620
- // src/output.ts
1621
- var JSON_SCHEMA_VERSION = 1;
1622
- var jsonSuccess = (command, data) => ({
1623
- schemaVersion: JSON_SCHEMA_VERSION,
1624
- command,
1625
- data
1626
- });
1627
- var jsonFailure = (command, error) => ({
1628
- schemaVersion: JSON_SCHEMA_VERSION,
1629
- command,
1630
- error
1631
- });
1632
- function renderJson(payload) {
1633
- return `${JSON.stringify(payload, null, 2)}
1634
- `;
1635
- }
1636
- function renderFields(fields) {
1637
- const width = Math.max(0, ...fields.map(([key]) => key.length));
1638
- return fields.map(([k, v]) => `${k.padEnd(width)} ${v}`).join("\n");
1639
- }
1640
- function renderTable(headers, rows) {
1641
- const widths = headers.map(
1642
- (header, column) => Math.max(header.length, ...rows.map((row) => (row[column] ?? "").length))
1643
- );
1644
- const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
1645
- return [line(headers), line(widths.map((w) => "-".repeat(w))), ...rows.map(line)].join("\n");
1646
- }
1647
-
1648
- // src/target.ts
1649
- import { statSync as statSync2 } from "fs";
1650
- import { dirname as dirname3, resolve as resolve2 } from "path";
1651
-
1652
- // src/config.ts
1653
- import {
1654
- closeSync,
1655
- constants,
1656
- existsSync,
1657
- fsyncSync,
1658
- lstatSync,
1659
- mkdirSync,
1660
- openSync,
1661
- readFileSync,
1662
- renameSync,
1663
- statSync,
1664
- unlinkSync,
1665
- writeFileSync
1666
- } from "fs";
1667
- import { homedir } from "os";
1668
- import { dirname, join } from "path";
1669
- function configPath(env = process.env) {
1670
- const base = env.HLIX_CONFIG_HOME ?? env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
1671
- return join(base, "hlix", "credentials.json");
1672
- }
1673
- function readCredential(env = process.env) {
1674
- const fromEnv = env.HLIX_API_KEY;
1675
- if (fromEnv) {
1676
- return {
1677
- baseUrl: env.HLIX_BASE_URL ?? "https://server.hlix.ai",
1678
- apiKey: fromEnv,
1679
- ...env.HLIX_WORKSPACE_ID ? { organizationId: env.HLIX_WORKSPACE_ID } : {}
1680
- };
1681
- }
1682
- try {
1683
- const parsed = JSON.parse(readFileSync(configPath(env), "utf8"));
1684
- return parsed.apiKey && parsed.baseUrl ? parsed : null;
1685
- } catch {
1686
- return null;
1687
- }
1688
- }
1689
- function storedCredentialFile(env = process.env) {
1690
- const path = configPath(env);
1691
- return existsSync(path) ? path : null;
1692
- }
1693
- function removeCredential(env = process.env) {
1694
- const path = configPath(env);
1695
- if (!existsSync(path)) return { path, removed: false, reason: "absent" };
1696
- const entry = lstatSync(path);
1697
- if (entry.isSymbolicLink() || !entry.isFile()) {
1698
- throw new Error("Refusing to remove a credential path that is not a regular file.");
1699
- }
1700
- const uid = process.getuid?.();
1701
- if (uid !== void 0 && statSync(path).uid !== uid) {
1702
- throw new Error("Refusing to remove a credential file owned by another user.");
1703
- }
1704
- let parsed;
1705
- try {
1706
- parsed = JSON.parse(readFileSync(path, "utf8"));
1707
- } catch {
1708
- parsed = null;
1709
- }
1710
- const document = parsed;
1711
- if (!document || typeof document.apiKey !== "string" || typeof document.baseUrl !== "string") {
1712
- throw new Error(`Refusing to remove ${path}: it is not a credential written by this CLI.`);
1713
- }
1714
- unlinkSync(path);
1715
- return { path, removed: true, reason: "removed" };
1716
- }
1717
- function writeCredential(credential, env = process.env) {
1718
- const path = configPath(env);
1719
- const directory = dirname(path);
1720
- mkdirSync(directory, { recursive: true, mode: 448 });
1721
- if (lstatSync(directory).isSymbolicLink()) {
1722
- throw new Error("Refusing to write credentials through a symlinked directory.");
1723
- }
1724
- if (existsSync(path)) {
1725
- const entry = lstatSync(path);
1726
- if (entry.isSymbolicLink() || !entry.isFile()) {
1727
- throw new Error("Refusing to replace a credential path that is not a regular file.");
1728
- }
1729
- const getuid = process.getuid?.();
1730
- if (getuid !== void 0 && statSync(path).uid !== getuid) {
1731
- throw new Error("Refusing to replace a credential file owned by another user.");
1732
- }
1733
- }
1734
- const temporary = join(directory, `.credentials.${process.pid}.${crypto.randomUUID()}.tmp`);
1735
- let fd;
1736
- try {
1737
- fd = openSync(
1738
- temporary,
1739
- constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
1740
- 384
1741
- );
1742
- writeFileSync(fd, `${JSON.stringify(credential, null, 2)}
1743
- `, "utf8");
1744
- fsyncSync(fd);
1745
- closeSync(fd);
1746
- fd = void 0;
1747
- renameSync(temporary, path);
1748
- const dirFd = openSync(directory, constants.O_RDONLY);
1749
- try {
1750
- try {
1751
- fsyncSync(dirFd);
1752
- } catch (error) {
1753
- const code = error.code;
1754
- if (code !== "EINVAL" && code !== "ENOTSUP") throw error;
1755
- }
1756
- } finally {
1757
- closeSync(dirFd);
1758
- }
1759
- } catch (error) {
1760
- if (fd !== void 0) closeSync(fd);
1761
- if (existsSync(temporary)) unlinkSync(temporary);
1762
- throw error;
1763
- }
1764
- return path;
1765
- }
1766
-
1767
- // src/project/config.ts
1768
- import {
1769
- constants as constants2,
1770
- existsSync as existsSync2,
1771
- lstatSync as lstatSync2,
1772
- mkdirSync as mkdirSync2,
1773
- openSync as openSync2,
1774
- closeSync as closeSync2,
1775
- fsyncSync as fsyncSync2,
1776
- readFileSync as readFileSync2,
1777
- renameSync as renameSync2,
1778
- unlinkSync as unlinkSync2,
1779
- writeFileSync as writeFileSync2
1780
- } from "fs";
1781
- import { dirname as dirname2, join as join2, resolve } from "path";
1782
- function assertRegularOrMissing(path) {
1783
- if (!existsSync2(path)) return;
1784
- const entry = lstatSync2(path);
1785
- if (entry.isSymbolicLink() || !entry.isFile()) {
1786
- throw new Error(`Refusing to replace a non-regular Hlix file: ${path}`);
1787
- }
1788
- }
1789
- function atomicJson(path, value) {
1790
- const directory = dirname2(path);
1791
- mkdirSync2(directory, { recursive: true, mode: 448 });
1792
- if (lstatSync2(directory).isSymbolicLink()) {
1793
- throw new Error(`Refusing to write through a symlinked Hlix directory: ${directory}`);
1794
- }
1795
- assertRegularOrMissing(path);
1796
- const temporary = join2(directory, `.hlix-${process.pid}-${crypto.randomUUID()}.tmp`);
1797
- let fd;
1798
- try {
1799
- fd = openSync2(
1800
- temporary,
1801
- constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW,
1802
- 384
1803
- );
1804
- writeFileSync2(fd, `${JSON.stringify(value, null, 2)}
1805
- `, "utf8");
1806
- fsyncSync2(fd);
1807
- closeSync2(fd);
1808
- fd = void 0;
1809
- renameSync2(temporary, path);
1810
- } catch (error) {
1811
- if (fd !== void 0) closeSync2(fd);
1812
- if (existsSync2(temporary)) unlinkSync2(temporary);
1813
- throw error;
1814
- }
1815
- }
1816
- function projectConfigPath(root) {
1817
- return join2(resolve(root), ".hlix", "config.json");
1818
- }
1819
- function projectStatePath(root) {
1820
- return join2(resolve(root), ".hlix", "state.json");
1821
- }
1822
- function readProjectConfig(root) {
1823
- try {
1824
- const parsed = JSON.parse(readFileSync2(projectConfigPath(root), "utf8"));
1825
- return parsed.schemaVersion === 1 && parsed.workspaceId && parsed.apiUrl ? parsed : null;
1826
- } catch {
1827
- return null;
1828
- }
1829
- }
1830
- function writeProjectConfig(root, config) {
1831
- atomicJson(projectConfigPath(root), config);
1832
- const ignorePath = join2(resolve(root), ".hlix", ".gitignore");
1833
- if (!existsSync2(ignorePath)) writeFileSync2(ignorePath, "state.json\n", { mode: 420, flag: "wx" });
1834
- const hlixIgnore = join2(resolve(root), ".hlixignore");
1835
- if (!existsSync2(hlixIgnore)) {
1836
- writeFileSync2(
1837
- hlixIgnore,
1838
- "# Additional project-local paths to exclude from Hlix snapshots.\n",
1839
- { mode: 420, flag: "wx" }
1840
- );
1841
- }
1842
- }
1843
- function writeProjectState(root, state) {
1844
- atomicJson(projectStatePath(root), state);
1845
- }
1846
- function readProjectState(root) {
1847
- try {
1848
- const parsed = JSON.parse(readFileSync2(projectStatePath(root), "utf8"));
1849
- return parsed.schemaVersion === 1 && parsed.revisionId && parsed.generation > 0 ? parsed : null;
1850
- } catch {
1851
- return null;
1852
- }
1853
- }
1854
-
1855
- // src/target.ts
1856
- var DEFAULT_BASE_URL = "https://server.hlix.ai";
1857
- function findProjectBinding(cwd) {
1858
- let root = resolve2(cwd);
1859
- for (; ; ) {
1860
- const config = readProjectConfig(root);
1861
- if (config) return { root, config };
1862
- const parent = dirname3(root);
1863
- if (parent === root) return null;
1864
- root = parent;
1865
- }
1866
- }
1867
- function isDirectory(path) {
1868
- try {
1869
- return statSync2(path).isDirectory();
1870
- } catch {
1871
- return false;
1872
- }
1873
- }
1874
- function text(value) {
1875
- if (typeof value !== "string") return void 0;
1876
- const trimmed = value.trim();
1877
- return trimmed ? trimmed : void 0;
1878
- }
1879
- var sameUrl = (a, b) => a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
1880
- function resolveTarget(input) {
1881
- const { env, flags, cwd } = input;
1882
- const binding2 = findProjectBinding(cwd);
1883
- const credential = readCredential(env);
1884
- const credentialSource = credential ? env.HLIX_API_KEY ? "env" : "file" : null;
1885
- const stored = credentialSource === "file" ? credential : null;
1886
- const flagBaseUrl = text(flags["base-url"]);
1887
- const envBaseUrl = text(env.HLIX_BASE_URL);
1888
- const flagWorkspace = text(flags.workspace);
1889
- const envWorkspace = text(env.HLIX_WORKSPACE_ID);
1890
- const baseUrl = flagBaseUrl ?? envBaseUrl ?? binding2?.config.apiUrl ?? stored?.baseUrl ?? DEFAULT_BASE_URL;
1891
- const baseUrlSource = flagBaseUrl ? "flag" : envBaseUrl ? "env" : binding2?.config.apiUrl ? "project" : stored?.baseUrl ? "credential" : "default";
1892
- const workspaceId = flagWorkspace ?? envWorkspace ?? binding2?.config.workspaceId ?? stored?.organizationId ?? null;
1893
- const workspaceSource = flagWorkspace ? "flag" : envWorkspace ? "env" : binding2?.config.workspaceId ? "project" : stored?.organizationId ? "credential" : null;
1894
- return {
1895
- baseUrl,
1896
- baseUrlSource,
1897
- workspaceId,
1898
- workspaceSource,
1899
- credential,
1900
- credentialSource,
1901
- credentialPath: configPath(env),
1902
- binding: binding2,
1903
- conflict: conflictWith(binding2, {
1904
- flagBaseUrl,
1905
- envBaseUrl,
1906
- flagWorkspace,
1907
- envWorkspace
1908
- })
1909
- };
1910
- }
1911
- function conflictWith(binding2, explicit) {
1912
- if (!binding2) return null;
1913
- const where = `${binding2.root} is bound to`;
1914
- const fix = "Drop the override, or point --cwd at a folder bound to it.";
1915
- if (explicit.flagWorkspace && explicit.flagWorkspace !== binding2.config.workspaceId) {
1916
- return `${where} workspace ${binding2.config.workspaceId}, but --workspace names ${explicit.flagWorkspace}. ${fix}`;
1917
- }
1918
- if (explicit.envWorkspace && explicit.envWorkspace !== binding2.config.workspaceId) {
1919
- return `${where} workspace ${binding2.config.workspaceId}, but HLIX_WORKSPACE_ID names ${explicit.envWorkspace}. ${fix}`;
1920
- }
1921
- if (explicit.flagBaseUrl && !sameUrl(explicit.flagBaseUrl, binding2.config.apiUrl)) {
1922
- return `${where} ${binding2.config.apiUrl}, but --base-url names ${explicit.flagBaseUrl}. ${fix}`;
1923
- }
1924
- if (explicit.envBaseUrl && !sameUrl(explicit.envBaseUrl, binding2.config.apiUrl)) {
1925
- return `${where} ${binding2.config.apiUrl}, but HLIX_BASE_URL names ${explicit.envBaseUrl}. ${fix}`;
1926
- }
1927
- return null;
1928
- }
1929
-
1930
- // src/cli.ts
1931
- function errorCode(error) {
1932
- if (error instanceof CliError) return { code: error.code, status: null };
1933
- if (error instanceof HlixValidationError) return { code: "invalid_request", status: 400 };
1934
- if (error instanceof HlixBadRequestError) return { code: "bad_request", status: 400 };
1935
- if (error instanceof HlixAuthenticationError) return { code: "unauthenticated", status: 401 };
1936
- if (error instanceof HlixPermissionError) return { code: "forbidden", status: 403 };
1937
- if (error instanceof HlixNotFoundError) return { code: "not_found", status: 404 };
1938
- if (error instanceof HlixConflictError) return { code: "conflict", status: 409 };
1939
- if (error instanceof HlixRateLimitError) return { code: "rate_limited", status: 429 };
1940
- if (error instanceof HlixNotImplementedError) return { code: "not_implemented", status: 501 };
1941
- if (error instanceof HlixServerError) {
1942
- return { code: "server_error", status: error.status };
1943
- }
1944
- if (error instanceof HlixTransportError) return { code: "unreachable", status: null };
1945
- return { code: "unknown", status: null };
1946
- }
1947
- var messageOf = (error) => error instanceof Error ? error.message : String(error);
1948
- function clientFor(env, options) {
1949
- const target = resolveTarget({ env, flags: options.flags, cwd: options.cwd });
1950
- if (target.conflict) throw new CliError("workspace_mismatch", target.conflict);
1951
- if (!target.credential) {
1952
- throw new CliError(
1953
- "unauthenticated",
1954
- "No credential. Run `hlix auth login`, or set HLIX_API_KEY for CI."
1955
- );
1956
- }
1957
- return new Hlix({
1958
- baseUrl: target.baseUrl,
1959
- credential: apiKeyCredential(target.credential.apiKey),
1960
- ...target.workspaceId ? { organizationId: target.workspaceId } : {}
1961
- });
1962
- }
1963
- function helpText(commands2) {
1964
- const width = Math.max(...commands2.map((c) => c.name.length));
1965
- const lines = commands2.map((c) => ` hlix ${c.name.padEnd(width)} ${c.summary}`).join("\n");
1966
- return `hlix \u2014 the hlix control-plane CLI
1967
-
1968
- Usage:
1969
- hlix <command> [options]
1970
-
1971
- Commands:
1972
- ${lines}
1973
-
1974
- Options:
1975
- --json Machine-readable output (a versioned, stable contract)
1976
- --cwd <dir> Operate on another directory instead of the current one
1977
- --workspace <id> Workspace to act in, overriding the folder's binding
1978
- --base-url <url> API to talk to, overriding the folder's binding
1979
- --help Show this help
1980
- --version Show the installed CLI version
1981
-
1982
- A command targets the workspace named by, in order: an explicit flag, the
1983
- HLIX_* environment, the nearest .hlix/config.json, then the stored credential.
1984
- Run \`hlix status\` to see which one applied.
1985
- `;
1986
- }
1987
- function match(commands2, argv) {
1988
- for (const command of [...commands2].sort((a, b) => b.name.length - a.name.length)) {
1989
- const words = command.name.split(" ");
1990
- if (words.every((word, i) => argv[i] === word)) {
1991
- return { command, rest: argv.slice(words.length) };
1992
- }
1993
- }
1994
- return null;
1995
- }
1996
- async function runCli(commands2, argv, io) {
1997
- if (argv.some((arg) => arg === "--api-key" || arg.startsWith("--api-key="))) {
1998
- io.stderr("error: API keys are never accepted as command-line arguments. Run `hlix auth login` and use the hidden prompt.\n");
1999
- return 2;
2000
- }
2001
- let parsed;
2002
- try {
2003
- parsed = parseArgs({
2004
- args: argv,
2005
- allowPositionals: true,
2006
- strict: true,
2007
- options: {
2008
- json: { type: "boolean" },
2009
- help: { type: "boolean" },
2010
- version: { type: "boolean" },
2011
- cwd: { type: "string" },
2012
- "base-url": { type: "string" },
2013
- workspace: { type: "string" },
2014
- "dry-run": { type: "boolean" },
2015
- "env-file": { type: "string" },
2016
- history: { type: "string" },
2017
- name: { type: "string" },
2018
- stack: { type: "string" },
2019
- status: { type: "string" },
2020
- project: { type: "string" },
2021
- yes: { type: "boolean" },
2022
- force: { type: "boolean" },
2023
- runtime: { type: "string" },
2024
- profile: { type: "string" },
2025
- server: { type: "string" },
2026
- "allow-stdio": { type: "boolean" }
2027
- }
2028
- });
2029
- } catch (error) {
2030
- const json2 = argv.includes("--json");
2031
- const message = messageOf(error);
2032
- if (json2) {
2033
- io.stdout(renderJson(jsonFailure("cli", { code: "invalid_usage", message, status: null })));
2034
- } else {
2035
- io.stderr(`${message}
2036
- `);
2037
- }
2038
- return 2;
2039
- }
2040
- const json = parsed.values.json === true;
2041
- const positionals = parsed.positionals;
2042
- if (parsed.values.version === true) {
2043
- const { version } = await import("./package-WT5SD5LO.js");
2044
- io.stdout(`${version}
2045
- `);
2046
- return 0;
2047
- }
2048
- if (positionals.length === 0 || parsed.values.help === true) {
2049
- const hit2 = match(commands2, positionals);
2050
- io.stdout(hit2 ? `${hit2.command.usage}
2051
- ` : helpText(commands2));
2052
- return positionals.length === 0 && parsed.values.help !== true ? 1 : 0;
2053
- }
2054
- const hit = match(commands2, positionals);
2055
- if (!hit) {
2056
- const name = positionals.join(" ");
2057
- if (json) {
2058
- io.stdout(
2059
- renderJson(
2060
- jsonFailure(name, {
2061
- code: "unknown_command",
2062
- message: `Unknown command: ${name}`,
2063
- status: null
2064
- })
2065
- )
2066
- );
2067
- } else {
2068
- io.stderr(`Unknown command: ${name}
2069
-
2070
- ${helpText(commands2)}`);
2071
- }
2072
- return 2;
2073
- }
2074
- const globalOptions = /* @__PURE__ */ new Set(["json", "help", "version", "cwd", "workspace", "base-url"]);
2075
- const allowedOptions = new Set(hit.command.options ?? []);
2076
- const unsupported = Object.entries(parsed.values).filter(([name, value]) => value !== void 0 && !globalOptions.has(name) && !allowedOptions.has(name)).map(([name]) => `--${name}`);
2077
- const bounds = hit.command.args ?? { min: 0, max: 0 };
2078
- const positionalError = hit.rest.length < bounds.min ? { code: "missing_argument", message: hit.command.usage.split("\n")[0] } : hit.rest.length > bounds.max ? { code: "invalid_usage", message: hit.command.usage.split("\n")[0] } : null;
2079
- const cwdFlag = typeof parsed.values.cwd === "string" ? parsed.values.cwd : void 0;
2080
- const cwd = resolve3(cwdFlag ?? ".");
2081
- const cwdError = cwdFlag !== void 0 && !isDirectory(cwd) ? { code: "invalid_usage", message: `--cwd is not a directory: ${cwd}` } : null;
2082
- const usageError = unsupported.length > 0 ? { code: "invalid_usage", message: `Unsupported option for ${hit.command.name}: ${unsupported.join(", ")}` } : cwdError ?? positionalError;
2083
- if (usageError) {
2084
- if (json) {
2085
- io.stdout(renderJson(jsonFailure(hit.command.name, { ...usageError, status: null })));
2086
- } else {
2087
- io.stderr(`error: ${usageError.message}
2088
-
2089
- ${hit.command.usage}
2090
- `);
2091
- }
2092
- return 2;
2093
- }
2094
- const flags = parsed.values;
2095
- const context = {
2096
- ...io,
2097
- args: hit.rest,
2098
- json,
2099
- flags,
2100
- cwd,
2101
- requireClient: (at) => clientFor(io.env, { cwd: at ?? cwd, flags })
2102
- };
2103
- try {
2104
- return await hit.command.run(context);
2105
- } catch (error) {
2106
- const { code, status } = errorCode(error);
2107
- if (json) {
2108
- io.stdout(
2109
- renderJson(
2110
- jsonFailure(hit.command.name, { code, message: messageOf(error), status })
2111
- )
2112
- );
2113
- } else {
2114
- io.stderr(`error: ${messageOf(error)}
2115
- `);
2116
- }
2117
- return code === "unauthenticated" || code === "forbidden" ? 3 : 1;
2118
- }
2119
- }
2120
-
2121
- // src/commands/auth.ts
2122
- var authLogin = {
2123
- name: "auth login",
2124
- summary: "Verify and store a credential for this machine",
2125
- usage: [
2126
- "Usage: hlix auth login --workspace <id> [--base-url <url>]",
2127
- "",
2128
- " --base-url Defaults to https://server.hlix.ai",
2129
- " --workspace Required. Workspace (organization) id to act in",
2130
- "",
2131
- "The API key is read from a hidden prompt and never accepted as an argument.",
2132
- "For CI, set HLIX_API_KEY and HLIX_WORKSPACE_ID in the environment."
2133
- ].join("\n"),
2134
- supportsJson: true,
2135
- options: ["base-url", "workspace"],
2136
- async run(ctx) {
2137
- const organizationId = typeof ctx.flags.workspace === "string" ? ctx.flags.workspace : ctx.env.HLIX_WORKSPACE_ID;
2138
- if (!organizationId) {
2139
- throw new CliError("missing_argument", "--workspace is required.");
2140
- }
2141
- const apiKey = ctx.env.HLIX_API_KEY ?? await ctx.readSecret?.("Hlix API key: ") ?? "";
2142
- if (!apiKey) {
2143
- throw new CliError(
2144
- "missing_credential",
2145
- "No API key entered. Run from a terminal or set HLIX_API_KEY for CI."
2146
- );
2147
- }
2148
- const baseUrl = typeof ctx.flags["base-url"] === "string" ? ctx.flags["base-url"] : ctx.env.HLIX_BASE_URL ?? DEFAULT_BASE_URL;
2149
- const parsedBaseUrl = new URL(baseUrl);
2150
- const loopback = parsedBaseUrl.hostname === "localhost" || parsedBaseUrl.hostname === "127.0.0.1" || parsedBaseUrl.hostname === "::1";
2151
- if (parsedBaseUrl.protocol !== "https:" && !(parsedBaseUrl.protocol === "http:" && loopback)) {
2152
- throw new CliError("invalid_base_url", "--base-url must use HTTPS (HTTP is allowed only for localhost).");
2153
- }
2154
- const probe = new Hlix({
2155
- baseUrl,
2156
- credential: apiKeyCredential(apiKey),
2157
- organizationId,
2158
- retry: { attempts: 1 }
2159
- });
2160
- await probe.projects.list();
2161
- const path = writeCredential({ baseUrl, apiKey, organizationId }, ctx.env);
2162
- if (ctx.json) {
2163
- ctx.stdout(
2164
- renderJson(jsonSuccess(authLogin.name, { baseUrl, organizationId, credentialPath: path }))
2165
- );
2166
- } else {
2167
- ctx.stdout(`Signed in to ${baseUrl}
2168
- Credential stored at ${path}
2169
- `);
2170
- }
2171
- return 0;
2172
- }
2173
- };
2174
- var authStatus = {
2175
- name: "auth status",
2176
- summary: "Show which credential is in use, where it came from, and whether it works",
2177
- usage: [
2178
- "Usage: hlix auth status [--json]",
2179
- "",
2180
- "Names the SOURCE of the credential \u2014 the environment or the stored file \u2014",
2181
- "and verifies it against the API. The key itself is never printed, not even",
2182
- "a prefix: machine output gets piped into logs."
2183
- ].join("\n"),
2184
- supportsJson: true,
2185
- async run(ctx) {
2186
- const target = resolveTarget({ env: ctx.env, flags: ctx.flags, cwd: ctx.cwd });
2187
- if (target.conflict) throw new CliError("workspace_mismatch", target.conflict);
2188
- if (!target.credential) {
2189
- throw new CliError(
2190
- "unauthenticated",
2191
- "No credential. Run `hlix auth login`, or set HLIX_API_KEY for CI."
2192
- );
2193
- }
2194
- try {
2195
- await ctx.requireClient().projects.list();
2196
- } catch (error) {
2197
- ctx.stderr(
2198
- `credential from ${target.credentialSource}, workspace ${target.workspaceId ?? "none"}, api ${target.baseUrl}
2199
- `
2200
- );
2201
- throw error;
2202
- }
2203
- const data = {
2204
- credentialSource: target.credentialSource,
2205
- credentialPath: target.credentialSource === "file" ? target.credentialPath : null,
2206
- baseUrl: target.baseUrl,
2207
- baseUrlSource: target.baseUrlSource,
2208
- workspaceId: target.workspaceId,
2209
- workspaceSource: target.workspaceSource,
2210
- valid: true
2211
- };
2212
- if (ctx.json) {
2213
- ctx.stdout(renderJson(jsonSuccess(authStatus.name, data)));
2214
- return 0;
2215
- }
2216
- ctx.stdout(
2217
- `${renderFields([
2218
- [
2219
- "credential",
2220
- target.credentialSource === "env" ? "environment (HLIX_API_KEY)" : `file (${target.credentialPath})`
2221
- ],
2222
- ["api", `${target.baseUrl} (from ${target.baseUrlSource})`],
2223
- [
2224
- "workspace",
2225
- target.workspaceId ? `${target.workspaceId} (from ${target.workspaceSource})` : "none"
2226
- ],
2227
- ["status", "valid"]
2228
- ])}
2229
- `
2230
- );
2231
- return 0;
2232
- }
2233
- };
2234
- var authLogout = {
2235
- name: "auth logout",
2236
- summary: "Remove the stored credential from this machine",
2237
- usage: [
2238
- "Usage: hlix auth logout [--yes] [--json]",
2239
- "",
2240
- "Removes ~/.config/hlix/credentials.json after confirming. A credential",
2241
- "supplied through HLIX_API_KEY is not removed \u2014 unset it in the shell that",
2242
- "set it."
2243
- ].join("\n"),
2244
- supportsJson: true,
2245
- options: ["yes"],
2246
- async run(ctx) {
2247
- const target = resolveTarget({ env: ctx.env, flags: ctx.flags, cwd: ctx.cwd });
2248
- if (target.credentialSource === "env") {
2249
- const stored = storedCredentialFile(ctx.env);
2250
- if (!stored) {
2251
- const data = { removed: false, reason: "environment", path: null };
2252
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(authLogout.name, data)));
2253
- else ctx.stdout("HLIX_API_KEY is set in this environment; unset it there to sign out.\n");
2254
- return 0;
2255
- }
2256
- if (ctx.flags.yes !== true) {
2257
- const data = { removed: false, reason: "environment", path: stored };
2258
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(authLogout.name, data)));
2259
- else {
2260
- ctx.stdout(
2261
- `HLIX_API_KEY is set in this environment; unset it there to sign out.
2262
- A stored credential also exists at ${stored} \u2014 repeat with --yes to remove it.
2263
- `
2264
- );
2265
- }
2266
- return 0;
2267
- }
2268
- const removed = removeCredential(ctx.env);
2269
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(authLogout.name, removed)));
2270
- else {
2271
- ctx.stdout(
2272
- `Removed ${removed.path}
2273
- HLIX_API_KEY is still set in this environment; unset it there to finish signing out.
2274
- `
2275
- );
2276
- }
2277
- return 0;
2278
- }
2279
- if (ctx.flags.yes !== true && target.credential) {
2280
- if (ctx.json || !ctx.confirm) {
2281
- throw new CliError(
2282
- "approval_required",
2283
- "Removing the stored credential needs approval. Repeat with `--yes`."
2284
- );
2285
- }
2286
- const approved = await ctx.confirm(
2287
- `Remove the stored credential at ${target.credentialPath}? Signing back in needs the API key again.`
2288
- );
2289
- if (!approved) throw new CliError("cancelled", "Logout cancelled.");
2290
- }
2291
- const removal = removeCredential(ctx.env);
2292
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(authLogout.name, removal)));
2293
- else {
2294
- ctx.stdout(
2295
- removal.removed ? `Removed ${removal.path}
2296
- ` : `No stored credential at ${removal.path}
2297
- `
2298
- );
2299
- }
2300
- return 0;
2301
- }
2302
- };
2303
-
2304
- // src/commands/projects.ts
2305
- function field(row, key) {
2306
- if (typeof row !== "object" || row === null) return "";
2307
- const value = row[key];
2308
- return value === null || value === void 0 ? "" : String(value);
2309
- }
2310
- var projectsList = {
2311
- name: "projects list",
2312
- summary: "List the projects in your workspace",
2313
- usage: "Usage: hlix projects list [--json]",
2314
- supportsJson: true,
2315
- async run(ctx) {
2316
- const projects = await ctx.requireClient().projects.list();
2317
- if (ctx.json) {
2318
- ctx.stdout(renderJson(jsonSuccess(projectsList.name, projects)));
2319
- return 0;
2320
- }
2321
- const rows = Array.isArray(projects) ? projects : [];
2322
- if (rows.length === 0) {
2323
- ctx.stdout("No projects.\n");
2324
- return 0;
2325
- }
2326
- ctx.stdout(
2327
- `${renderTable(
2328
- ["ID", "NAME", "STACK"],
2329
- rows.map((row) => [field(row, "id"), field(row, "name"), field(row, "stack")])
2330
- )}
2331
- `
2332
- );
2333
- return 0;
2334
- }
2335
- };
2336
- var projectsGet = {
2337
- name: "projects get",
2338
- summary: "Show one project, defaulting to the one this folder is bound to",
2339
- usage: [
2340
- "Usage: hlix projects get [project-id] [--json]",
2341
- "",
2342
- "With no id, the project this folder is bound to is used."
2343
- ].join("\n"),
2344
- supportsJson: true,
2345
- args: { min: 0, max: 1 },
2346
- async run(ctx) {
2347
- const id = ctx.args[0] ?? findProjectBinding(ctx.cwd)?.config.projectId;
2348
- if (!id) {
2349
- throw new CliError(
2350
- "missing_argument",
2351
- "A project id is required, or run this inside a folder bound to a project."
2352
- );
2353
- }
2354
- const project = await ctx.requireClient().projects.get(id);
2355
- if (ctx.json) {
2356
- ctx.stdout(renderJson(jsonSuccess(projectsGet.name, project)));
2357
- return 0;
2358
- }
2359
- const repos = project.repos;
2360
- ctx.stdout(
2361
- `${renderFields(
2362
- [
2363
- ["id", field(project, "id")],
2364
- ["name", field(project, "name")],
2365
- ["stack", field(project, "stack")],
2366
- ["status", field(project, "status")],
2367
- ["repos", Array.isArray(repos) ? String(repos.length) : ""]
2368
- ].filter(([, value]) => value !== "")
2369
- )}
2370
- `
2371
- );
2372
- return 0;
2373
- }
2374
- };
2375
-
2376
- // src/commands/tasks.ts
2377
- function requireId(ctx, what) {
2378
- const id = ctx.args[0];
2379
- if (!id) throw new CliError("missing_argument", `A ${what} id is required.`);
2380
- return id;
2381
- }
2382
- function flag(ctx, name) {
2383
- const value = ctx.flags[name];
2384
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
2385
- }
2386
- function field2(row, key) {
2387
- if (typeof row !== "object" || row === null) return "";
2388
- const value = row[key];
2389
- return value === null || value === void 0 ? "" : String(value);
2390
- }
2391
- var tasksList = {
2392
- name: "tasks list",
2393
- summary: "List tasks, defaulting to the project this folder is bound to",
2394
- usage: [
2395
- "Usage: hlix tasks list [--project <id>] [--status <status>] [--json]",
2396
- "",
2397
- "With no --project, a folder bound to a project lists that project's tasks;",
2398
- "anywhere else it lists the workspace's tasks."
2399
- ].join("\n"),
2400
- supportsJson: true,
2401
- options: ["project", "status"],
2402
- async run(ctx) {
2403
- const projectId = flag(ctx, "project") ?? findProjectBinding(ctx.cwd)?.config.projectId;
2404
- const status = flag(ctx, "status");
2405
- const tasks = await ctx.requireClient().tasks.list({
2406
- ...projectId ? { projectId } : {},
2407
- ...status ? { status } : {}
2408
- });
2409
- if (ctx.json) {
2410
- ctx.stdout(renderJson(jsonSuccess(tasksList.name, tasks)));
2411
- return 0;
2412
- }
2413
- const rows = Array.isArray(tasks) ? tasks : [];
2414
- if (rows.length === 0) {
2415
- ctx.stdout("No tasks.\n");
2416
- return 0;
2417
- }
2418
- ctx.stdout(
2419
- `${renderTable(
2420
- ["ID", "STATUS", "TITLE"],
2421
- rows.map((row) => [
2422
- field2(row, "id"),
2423
- field2(row, "status"),
2424
- field2(row, "title") || field2(row, "description").split("\n")[0] || ""
2425
- ])
2426
- )}
2427
- `
2428
- );
2429
- return 0;
2430
- }
2431
- };
2432
- var tasksGet = {
2433
- name: "tasks get",
2434
- summary: "Show one task",
2435
- usage: "Usage: hlix tasks get <task-id> [--json]",
2436
- supportsJson: true,
2437
- args: { min: 1, max: 1 },
2438
- async run(ctx) {
2439
- const id = requireId(ctx, "task");
2440
- const task = await ctx.requireClient().tasks.get(id);
2441
- if (ctx.json) {
2442
- ctx.stdout(renderJson(jsonSuccess(tasksGet.name, task)));
2443
- return 0;
2444
- }
2445
- ctx.stdout(
2446
- `${renderFields([
2447
- ["id", field2(task, "id")],
2448
- ["status", field2(task, "status")],
2449
- ["title", field2(task, "title")],
2450
- ["project", field2(task, "projectId")],
2451
- ["description", field2(task, "description")]
2452
- ])}
2453
- `
2454
- );
2455
- return 0;
2456
- }
2457
- };
2458
- var tasksReview = {
2459
- name: "tasks review",
2460
- summary: "Show the verified review evidence for a task",
2461
- usage: "Usage: hlix tasks review <task-id> [--json]",
2462
- supportsJson: true,
2463
- args: { min: 1, max: 1 },
2464
- async run(ctx) {
2465
- const id = requireId(ctx, "task");
2466
- const evidence = await ctx.requireClient().tasks.review(id);
2467
- if (ctx.json) {
2468
- ctx.stdout(renderJson(jsonSuccess(tasksReview.name, evidence)));
2469
- return 0;
2470
- }
2471
- ctx.stdout(`${JSON.stringify(evidence, null, 2)}
2472
- `);
2473
- return 0;
2474
- }
2475
- };
2476
- var tasksWatch = {
2477
- name: "tasks watch",
2478
- summary: "Stream a task's status changes until it finishes",
2479
- usage: [
2480
- "Usage: hlix tasks watch <task-id> [--json]",
2481
- "",
2482
- "Streams the task's server-sent status frames and exits when the task",
2483
- "reaches a terminal status or the server closes the stream.",
2484
- "",
2485
- "With --json, each frame is printed as one envelope per line (JSONL) so a",
2486
- "consumer can read them as they arrive."
2487
- ].join("\n"),
2488
- supportsJson: true,
2489
- args: { min: 1, max: 1 },
2490
- async run(ctx) {
2491
- const id = requireId(ctx, "task");
2492
- const client = ctx.requireClient();
2493
- for await (const event of client.tasks.stream(id)) {
2494
- let payload;
2495
- try {
2496
- payload = JSON.parse(event.data);
2497
- } catch {
2498
- payload = event.data;
2499
- }
2500
- if (ctx.json) {
2501
- ctx.stdout(`${JSON.stringify(jsonSuccess(tasksWatch.name, payload))}
2502
- `);
2503
- } else {
2504
- const status = field2(payload, "status");
2505
- const error = field2(payload, "error");
2506
- ctx.stdout(error ? `error: ${error}
2507
- ` : `${status || event.data}
2508
- `);
2509
- }
2510
- }
2511
- return 0;
2512
- }
2513
- };
2514
-
2515
- // src/prompt.ts
2516
- import { emitKeypressEvents } from "readline";
2517
- import { createInterface } from "readline/promises";
2518
- async function promptSecret(prompt) {
2519
- if (!process.stdin.isTTY || !process.stdin.setRawMode) {
2520
- throw new Error("Interactive login requires a terminal. Set HLIX_API_KEY for CI.");
2521
- }
2522
- process.stderr.write(prompt);
2523
- emitKeypressEvents(process.stdin);
2524
- process.stdin.setRawMode(true);
2525
- process.stdin.resume();
2526
- return new Promise((resolve9, reject) => {
2527
- let value = "";
2528
- const cleanup = () => {
2529
- process.stdin.off("keypress", onKeypress);
2530
- process.stdin.setRawMode(false);
2531
- process.stdin.pause();
2532
- process.stderr.write("\n");
2533
- };
2534
- const onKeypress = (character, key) => {
2535
- if (key.ctrl && key.name === "c" || key.name === "escape") {
2536
- cleanup();
2537
- reject(new Error("Login cancelled."));
2538
- return;
2539
- }
2540
- if (key.name === "return" || key.name === "enter") {
2541
- cleanup();
2542
- resolve9(value);
2543
- return;
2544
- }
2545
- if (key.name === "backspace") {
2546
- value = value.slice(0, -1);
2547
- return;
2548
- }
2549
- if (character && !key.ctrl) value += character;
2550
- };
2551
- process.stdin.on("keypress", onKeypress);
2552
- });
2553
- }
2554
- async function promptConfirm(prompt) {
2555
- if (!process.stdin.isTTY || !process.stderr.isTTY) {
2556
- throw new Error("Interactive approval requires a terminal. Review with --dry-run, then pass --yes.");
2557
- }
2558
- const terminal = createInterface({ input: process.stdin, output: process.stderr });
2559
- try {
2560
- const answer = (await terminal.question(`${prompt}
2561
- Continue? [y/N] `)).trim().toLowerCase();
2562
- return answer === "y" || answer === "yes";
2563
- } finally {
2564
- terminal.close();
2565
- }
2566
- }
2567
-
2568
- // src/commands/init.ts
2569
- import { realpath as realpath2 } from "fs/promises";
2570
- import { resolve as resolve5 } from "path";
2571
-
2572
- // src/project/scan.ts
2573
- var import_ignore = __toESM(require_ignore(), 1);
2574
- import { createHash } from "crypto";
2575
- import { createReadStream } from "fs";
2576
- import { lstat, readFile as readFile2, readdir, realpath, stat } from "fs/promises";
2577
- import { basename, join as join4, relative, resolve as resolve4, sep } from "path";
2578
- import { spawnSync } from "child_process";
2579
-
2580
- // src/project/environment.ts
2581
- import { readFile } from "fs/promises";
2582
- import { join as join3 } from "path";
2583
-
2584
- // src/project/gitpod.ts
2585
- function yamlScalar(raw) {
2586
- const value = raw.trim();
2587
- if (!value || /^[|>[{]/.test(value)) return null;
2588
- const quoted = value.match(/^(["'])([\s\S]*)\1$/);
2589
- if (!quoted && /^["']/.test(value)) return null;
2590
- return (quoted ? quoted[2].trim() : value.replace(/(?:^|\s)#.*$/, "").trim()) || null;
2591
- }
2592
- function gitpodTasks(source) {
2593
- const lines = source.split(/\r?\n/);
2594
- const header = lines.findIndex((line) => /^tasks:\s*(?:#.*)?$/.test(line));
2595
- if (header === -1) return { tasks: [], partial: /^tasks:\s*\S/m.test(source) };
2596
- const tasks = [];
2597
- let partial = false;
2598
- let fieldIndent = -1;
2599
- for (const line of lines.slice(header + 1)) {
2600
- if (!line.trim() || /^\s*#/.test(line)) continue;
2601
- const indent = line.length - line.trimStart().length;
2602
- const marker = line.match(/^(\s*-(?:\s+|$))(.*)$/);
2603
- if (marker) {
2604
- if (fieldIndent !== -1 && indent >= fieldIndent) continue;
2605
- tasks.push({});
2606
- fieldIndent = marker[2] ? marker[1].length : -1;
2607
- if (!marker[2]) continue;
2608
- } else if (indent === 0) break;
2609
- else if (tasks.length === 0) continue;
2610
- else if (fieldIndent === -1) fieldIndent = indent;
2611
- else if (indent !== fieldIndent) continue;
2612
- const field3 = (marker ? marker[2] : line.trim()).match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
2613
- if (!field3) continue;
2614
- const key = field3[1];
2615
- if (key !== "name" && key !== "before" && key !== "init" && key !== "command") continue;
2616
- const value = yamlScalar(field3[2]);
2617
- if (value === null) partial = true;
2618
- else tasks[tasks.length - 1][key] = value;
2619
- }
2620
- return { tasks, partial };
2621
- }
2622
-
2623
- // src/project/environment.ts
2624
- async function text2(root, path) {
2625
- try {
2626
- return await readFile(join3(root, ...path.split("/")), "utf8");
2627
- } catch (error) {
2628
- if (error.code === "ENOENT") return null;
2629
- throw error;
2630
- }
2631
- }
2632
- function jsonc(input) {
2633
- try {
2634
- const withoutComments = input.replace(/(^|[^:\\])\/\/.*$/gm, "$1").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
2635
- return JSON.parse(withoutComments);
2636
- } catch {
2637
- return null;
2638
- }
2639
- }
2640
- function commands(value) {
2641
- if (typeof value === "string" && value.trim()) return [value.trim()];
2642
- if (Array.isArray(value)) return value.flatMap(commands);
2643
- if (value && typeof value === "object") {
2644
- return Object.values(value).flatMap(commands);
2645
- }
2646
- return [];
2647
- }
2648
- function secretKeys(value) {
2649
- if (!value || typeof value !== "object") return [];
2650
- return Object.keys(value).filter((key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key));
2651
- }
2652
- function envReferenceKeys(content, includeTemplateReferences) {
2653
- const keys = /* @__PURE__ */ new Set();
2654
- const patterns = [
2655
- /\bprocess\.env\.([A-Za-z_][A-Za-z0-9_]*)/g,
2656
- /\bimport\.meta\.env\.([A-Za-z_][A-Za-z0-9_]*)/g,
2657
- /\bos\.(?:getenv|environ\.get)\(\s*["']([A-Za-z_][A-Za-z0-9_]*)/g,
2658
- /\bos\.Getenv\(\s*["']([A-Za-z_][A-Za-z0-9_]*)/g,
2659
- /\bENV\[\s*["']([A-Za-z_][A-Za-z0-9_]*)/g
2660
- ];
2661
- if (includeTemplateReferences) {
2662
- patterns.push(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::[-?][^}]*)?\}/g);
2663
- }
2664
- for (const pattern of patterns) {
2665
- for (const match2 of content.matchAll(pattern)) keys.add(match2[1]);
2666
- }
2667
- return [...keys];
2668
- }
2669
- function packageManagerInstall(paths, packageManager) {
2670
- if (paths.has("bun.lock") || paths.has("bun.lockb") || packageManager?.startsWith("bun@")) {
2671
- return "bun install --frozen-lockfile";
2672
- }
2673
- if (paths.has("pnpm-lock.yaml") || packageManager?.startsWith("pnpm@")) {
2674
- return "pnpm install --frozen-lockfile";
2675
- }
2676
- if (paths.has("yarn.lock") || packageManager?.startsWith("yarn@")) {
2677
- return "yarn install --immutable";
2678
- }
2679
- if (paths.has("package-lock.json") || paths.has("npm-shrinkwrap.json")) return "npm ci";
2680
- if (paths.has("package.json")) return "npm install";
2681
- return null;
2682
- }
2683
- function safePort(value) {
2684
- const port = typeof value === "number" ? value : Number(value);
2685
- return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
2686
- }
2687
- function addSource(sources, path, kind) {
2688
- if (!sources.some((source) => source.path === path && source.kind === kind)) sources.push({ path, kind });
2689
- }
2690
- async function discoverEnvironment(root, paths, readableText) {
2691
- const sources = [];
2692
- const install = [];
2693
- const build = [];
2694
- const terminals = [];
2695
- const ports = /* @__PURE__ */ new Map();
2696
- const required = /* @__PURE__ */ new Set();
2697
- const observed = /* @__PURE__ */ new Set();
2698
- const warnings = [];
2699
- const toolchain = {};
2700
- let start = null;
2701
- let gitpodBefore = 0;
2702
- const cursorText = await text2(root, ".cursor/environment.json");
2703
- if (cursorText) {
2704
- addSource(sources, ".cursor/environment.json", "cursor");
2705
- const config = jsonc(cursorText);
2706
- if (!config) warnings.push("Could not parse .cursor/environment.json; it remains quarantined.");
2707
- else {
2708
- install.push(...commands(config.install));
2709
- const starts = commands(config.start);
2710
- if (starts[0]) start = starts[0];
2711
- if (Array.isArray(config.terminals)) {
2712
- for (const raw of config.terminals) {
2713
- if (!raw || typeof raw !== "object") continue;
2714
- const item = raw;
2715
- if (typeof item.command !== "string" || !item.command.trim()) continue;
2716
- terminals.push({
2717
- name: typeof item.name === "string" && item.name.trim() ? item.name.trim() : "Terminal",
2718
- command: item.command.trim(),
2719
- ...typeof item.cwd === "string" && item.cwd.trim() ? { cwd: item.cwd.trim() } : {}
2720
- });
2721
- }
2722
- }
2723
- }
2724
- }
2725
- const devcontainerPath = paths.has(".devcontainer/devcontainer.json") ? ".devcontainer/devcontainer.json" : paths.has("devcontainer.json") ? "devcontainer.json" : null;
2726
- if (devcontainerPath) {
2727
- addSource(sources, devcontainerPath, "devcontainer");
2728
- const config = jsonc(readableText.get(devcontainerPath) ?? await text2(root, devcontainerPath) ?? "");
2729
- if (!config) warnings.push(`Could not parse ${devcontainerPath}; it remains quarantined.`);
2730
- else {
2731
- install.push(
2732
- ...commands(config.onCreateCommand),
2733
- ...commands(config.updateContentCommand),
2734
- ...commands(config.postCreateCommand)
2735
- );
2736
- const postStart = commands(config.postStartCommand);
2737
- if (!start && postStart[0]) start = postStart[0];
2738
- for (const key of secretKeys(config.secrets)) required.add(key);
2739
- for (const key of [...secretKeys(config.containerEnv), ...secretKeys(config.remoteEnv)]) required.add(key);
2740
- if (Array.isArray(config.forwardPorts)) {
2741
- for (const value of config.forwardPorts) {
2742
- const port = safePort(value);
2743
- if (port) ports.set(port, { port, visibility: "private" });
2744
- }
2745
- }
2746
- }
2747
- }
2748
- if (paths.has(".gitpod.yml")) {
2749
- addSource(sources, ".gitpod.yml", "gitpod");
2750
- const gitpodText = readableText.get(".gitpod.yml") ?? await text2(root, ".gitpod.yml");
2751
- if (gitpodText === null) warnings.push("Could not read .gitpod.yml; its task commands were not imported.");
2752
- else {
2753
- const gitpod = gitpodTasks(gitpodText);
2754
- gitpodBefore = gitpod.tasks.filter((task) => task.before).length;
2755
- for (const task of gitpod.tasks) {
2756
- if (task.before) install.push(task.before);
2757
- if (task.init) install.push(task.init);
2758
- if (!task.command) continue;
2759
- if (!start) start = task.command;
2760
- else terminals.push({ name: task.name ?? "Terminal", command: task.command });
2761
- }
2762
- if (gitpod.partial) {
2763
- warnings.push(
2764
- "Some .gitpod.yml task values are not plain `key: value` scalars and were not imported; review .gitpod.yml before approving cloud setup."
2765
- );
2766
- }
2767
- }
2768
- }
2769
- for (const path of ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]) {
2770
- if (paths.has(path)) addSource(sources, path, "docker");
2771
- }
2772
- for (const path of ["project.toml", "nixpacks.toml", "railpack.json", "Aptfile"]) {
2773
- if (paths.has(path)) addSource(sources, path, "buildpack");
2774
- }
2775
- for (const path of ["fly.toml", "railway.json", "render.yaml", "vercel.json", "netlify.toml"]) {
2776
- if (paths.has(path)) addSource(sources, path, "platform");
2777
- }
2778
- const packageText = readableText.get("package.json") ?? await text2(root, "package.json");
2779
- if (packageText) {
2780
- addSource(sources, "package.json", "package");
2781
- try {
2782
- const pkg = JSON.parse(packageText);
2783
- if (pkg.engines?.node) toolchain.node = pkg.engines.node;
2784
- const inferredInstall = packageManagerInstall(paths, pkg.packageManager);
2785
- if (inferredInstall && install.length === gitpodBefore) install.push(inferredInstall);
2786
- if (pkg.scripts?.build && build.length === 0) build.push(`${pkg.packageManager?.split("@")[0] ?? (paths.has("bun.lock") ? "bun" : "npm")} run build`);
2787
- const dev = pkg.scripts?.dev ? `${paths.has("bun.lock") ? "bun" : "npm"} run dev` : null;
2788
- const run2 = pkg.scripts?.start ? `${paths.has("bun.lock") ? "bun" : "npm"} run start` : dev;
2789
- if (!start && run2) start = run2;
2790
- } catch {
2791
- warnings.push("Could not parse package.json for environment discovery.");
2792
- }
2793
- }
2794
- if (paths.has("uv.lock")) install.push("uv sync --frozen");
2795
- else if (paths.has("poetry.lock")) install.push("poetry install --no-interaction");
2796
- else if (paths.has("requirements.txt")) install.push("python -m pip install -r requirements.txt");
2797
- if (paths.has("go.mod")) install.push("go mod download");
2798
- if (paths.has("Procfile")) {
2799
- addSource(sources, "Procfile", "procfile");
2800
- const procfile = readableText.get("Procfile") ?? "";
2801
- const web = procfile.split(/\r?\n/).find((line) => /^web\s*:/.test(line));
2802
- if (web) start = web.replace(/^web\s*:\s*/, "").trim() || start;
2803
- }
2804
- const versions = [
2805
- [".node-version", "node"],
2806
- [".nvmrc", "node"],
2807
- [".python-version", "python"]
2808
- ];
2809
- for (const [path, key] of versions) {
2810
- const value = (readableText.get(path) ?? await text2(root, path))?.trim();
2811
- if (value) {
2812
- toolchain[key] = value;
2813
- addSource(sources, path, "tool-version");
2814
- }
2815
- }
2816
- const toolVersions = readableText.get(".tool-versions") ?? await text2(root, ".tool-versions");
2817
- if (toolVersions) {
2818
- addSource(sources, ".tool-versions", "tool-version");
2819
- for (const raw of toolVersions.split(/\r?\n/)) {
2820
- const [name, value] = raw.trim().split(/\s+/, 2);
2821
- if (!value) continue;
2822
- if (name === "nodejs") toolchain.node = value;
2823
- else if (name === "python") toolchain.python = value;
2824
- else if (name === "bun") toolchain.bun = value;
2825
- }
2826
- }
2827
- const mise = readableText.get("mise.toml") ?? await text2(root, "mise.toml");
2828
- if (mise) {
2829
- addSource(sources, "mise.toml", "tool-version");
2830
- for (const match2 of mise.matchAll(/^\s*(node|python|bun)\s*=\s*["']([^"']+)["']/gm)) {
2831
- toolchain[match2[1]] = match2[2];
2832
- }
2833
- }
2834
- for (const [path, content] of readableText) {
2835
- const includeTemplateReferences = /(?:^|\/)(?:Dockerfile|Procfile)$/i.test(path) || /\.(?:jsonc?|ya?ml|toml|ini|conf|config|env|sh|bash|zsh|fish)$/i.test(path);
2836
- for (const key of envReferenceKeys(content, includeTemplateReferences)) observed.add(key);
2837
- for (const match2 of content.matchAll(/(?:--port(?:=|\s+)|PORT\s*=\s*)(\d{2,5})/g)) {
2838
- const port = safePort(match2[1]);
2839
- if (port) ports.set(port, { port, visibility: "private" });
2840
- }
2841
- }
2842
- if (sources.length === 0 && paths.size > 0) addSource(sources, "repository", "detected");
2843
- const dedupe = (values) => [...new Set(values.filter(Boolean))];
2844
- const signalCount = sources.length + install.length + (start ? 1 : 0);
2845
- return {
2846
- sources: sources.sort((a, b) => a.path.localeCompare(b.path)),
2847
- toolchain,
2848
- install: dedupe(install),
2849
- build: dedupe(build),
2850
- start,
2851
- terminals,
2852
- ports: [...ports.values()].sort((a, b) => a.port - b.port),
2853
- requiredSecretKeys: [...required].sort(),
2854
- observedSecretKeys: [...observed].sort(),
2855
- confidence: signalCount >= 4 ? "high" : signalCount >= 2 ? "medium" : "low",
2856
- warnings
2857
- };
2858
- }
2859
-
2860
- // src/project/manifest.ts
2861
- var PROJECT_MANIFEST_VERSION = 2;
2862
-
2863
- // src/project/scan.ts
2864
- var DEFAULT_MAX_FILES = 25e3;
2865
- var DEFAULT_MAX_FILE_BYTES = 50 * 1024 * 1024;
2866
- var DEFAULT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
2867
- var HISTORY_COMMIT_LIMIT = 1e4;
2868
- var MAX_PROTECTED_FILE_BYTES = 1024 * 1024;
2869
- var MAX_PROTECTED_TOTAL_BYTES = 10 * 1024 * 1024;
2870
- var DEFAULT_IGNORES = [
2871
- ".git/",
2872
- ".hlix/",
2873
- "node_modules/",
2874
- ".next/",
2875
- ".turbo/",
2876
- ".cache/",
2877
- "dist/",
2878
- "build/",
2879
- "coverage/",
2880
- "__pycache__/",
2881
- ".venv/",
2882
- "venv/",
2883
- "target/",
2884
- ".DS_Store"
2885
- ];
2886
- var SAFE_ENV_EXAMPLE = /(^|\/)\.env\.(example|sample|template)$/i;
2887
- var DOTENV_FILE = /(^|\/)\.env(?:\.[^/]+)?$/i;
2888
- var CREDENTIAL_FILE = /(^|\/)(?:\.envrc|\.netrc|\.npmrc|\.pypirc|credentials(?:\.[^/]+)?(?:\.json|\.toml)?|service-account[^/]*\.json|firebase-adminsdk[^/]*\.json|application_default_credentials\.json|\.docker\/config\.json|\.cargo\/credentials(?:\.toml)?|\.gem\/credentials|\.bundle\/config|(?:pip|pypi)\.conf|\.aws\/credentials|[^/]+\.(?:pem|key|p12|pfx)|terraform\.tfvars(?:\.json)?)$/i;
2889
- var MCP_FILE = /(^|\/)(?:\.mcp\.json|mcp\.json|claude_desktop_config\.json)$/i;
2890
- var AGENT_RESOURCE = /(^|\/)(?:AGENTS\.md|CLAUDE\.md|\.cursorrules|\.github\/copilot-instructions\.md)$/i;
2891
- var SKILL_RESOURCE = /(^|\/)(?:\.agents|\.claude)\/skills\//i;
2892
- var HIGH_CONFIDENCE_SECRET = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|npm_[A-Za-z0-9]{30,}|gh[opusr]_[A-Za-z0-9_]{30,}|glpat-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,}|(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{20,}/;
2893
- var STRUCTURED_CONFIG = /\.(?:json|ya?ml|toml|ini|conf|config)$/i;
2894
- var STRUCTURED_LITERAL_SECRET = /(?:^|[,{\s])(?:["']?(?:api[_-]?key|access[_-]?token|auth(?:orization)?|client[_-]?secret|password|private[_-]?key|secret|token)["']?)\s*(?::|=)\s*["']?(?!\$\{|process\.env|import\.meta\.env|os\.(?:getenv|environ\.get)|<|replace|example|changeme)[A-Za-z0-9+/_=.:-]{12,}/im;
2895
- var RESERVED_CONTROL_ENV = /^HLIX_[A-Za-z0-9_]*$/;
2896
- var AMBIENT_RUNTIME_ENV = /* @__PURE__ */ new Set([
2897
- "CI",
2898
- "HOME",
2899
- "LANG",
2900
- "LOGNAME",
2901
- "NODE_ENV",
2902
- "OLDPWD",
2903
- "PATH",
2904
- "PORT",
2905
- "PWD",
2906
- "SHELL",
2907
- "SHLVL",
2908
- "TERM",
2909
- "TMPDIR",
2910
- "USER",
2911
- "_"
2912
- ]);
2913
- function posixPath(path) {
2914
- return path.split(sep).join("/");
2915
- }
2916
- function git(root, args, maxBuffer = 10 * 1024 * 1024) {
2917
- return spawnSync("git", ["-C", root, ...args], {
2918
- encoding: "utf8",
2919
- maxBuffer,
2920
- stdio: ["ignore", "pipe", "pipe"]
2921
- });
2922
- }
2923
- async function sha256(path) {
2924
- const hash2 = createHash("sha256");
2925
- for await (const chunk of createReadStream(path)) hash2.update(chunk);
2926
- return hash2.digest("hex");
2927
- }
2928
- function stable(value) {
2929
- if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
2930
- if (value && typeof value === "object") {
2931
- return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(",")}}`;
2932
- }
2933
- return JSON.stringify(value);
2934
- }
2935
- function parseDotenv(content) {
2936
- const values = {};
2937
- for (const [index, rawLine] of String(content).replace(/^\uFEFF/, "").split(/\r?\n/).entries()) {
2938
- const line = rawLine.trim();
2939
- if (!line || line.startsWith("#")) continue;
2940
- const assignment = line.startsWith("export ") ? line.slice(7).trimStart() : line;
2941
- const match2 = assignment.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
2942
- if (!match2) throw new Error(`Invalid dotenv assignment on line ${index + 1}`);
2943
- let value = match2[2] ?? "";
2944
- if (value.startsWith('"')) {
2945
- if (!value.endsWith('"')) throw new Error(`Unclosed dotenv quote on line ${index + 1}`);
2946
- value = value.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r");
2947
- } else if (value.startsWith("'")) {
2948
- if (!value.endsWith("'")) throw new Error(`Unclosed dotenv quote on line ${index + 1}`);
2949
- value = value.slice(1, -1);
2950
- } else {
2951
- value = value.replace(/\s+#.*$/, "").trimEnd();
2952
- }
2953
- values[match2[1]] = value;
2954
- }
2955
- return values;
2956
- }
2957
- function hasShellExpansion(rawValue) {
2958
- let quote = null;
2959
- for (let index = 0; index < rawValue.length; index += 1) {
2960
- const char = rawValue[index];
2961
- if (char === "\\" && quote !== "single") {
2962
- if (rawValue[index + 1] !== "$") return true;
2963
- index += 1;
2964
- continue;
2965
- }
2966
- if (char === "'" && quote !== "double") {
2967
- quote = quote === "single" ? null : "single";
2968
- continue;
2969
- }
2970
- if (char === '"' && quote !== "single") {
2971
- quote = quote === "double" ? null : "double";
2972
- continue;
2973
- }
2974
- if (quote !== "single" && (char === "$" || char === "`")) return true;
2975
- if (quote === null && (char === "<" || char === ">") && rawValue[index + 1] === "(") return true;
2976
- }
2977
- return quote !== null;
2978
- }
2979
- function parseEnvrcLiterals(content) {
2980
- const values = {};
2981
- const keys = /* @__PURE__ */ new Set();
2982
- const dynamicKeys = /* @__PURE__ */ new Set();
2983
- for (const rawLine of String(content).replace(/^\uFEFF/, "").split(/\r?\n/)) {
2984
- const line = rawLine.trim();
2985
- if (!line || line.startsWith("#")) continue;
2986
- const assignment = line.startsWith("export ") ? line.slice(7).trimStart() : line;
2987
- const match2 = assignment.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
2988
- if (!match2) continue;
2989
- const key = match2[1];
2990
- const rawValue = match2[2] ?? "";
2991
- keys.add(key);
2992
- if (hasShellExpansion(rawValue)) {
2993
- delete values[key];
2994
- dynamicKeys.add(key);
2995
- continue;
2996
- }
2997
- try {
2998
- const parsed = parseDotenv(`${key}=${rawValue}`);
2999
- const value = parsed[key];
3000
- if (value === void 0) throw new Error("missing literal");
3001
- values[key] = rawValue.trimStart().startsWith("'") ? value : value.replace(/\\\$/g, "$");
3002
- dynamicKeys.delete(key);
3003
- } catch {
3004
- delete values[key];
3005
- dynamicKeys.add(key);
3006
- }
3007
- }
3008
- return { values, keys: [...keys].sort(), dynamicKeys: [...dynamicKeys].sort() };
3009
- }
3010
- async function isSelectedGitRoot(root) {
3011
- const result = git(root, ["rev-parse", "--show-toplevel"]);
3012
- if (result.status !== 0) return false;
3013
- try {
3014
- return await realpath(result.stdout.trim()) === root;
3015
- } catch {
3016
- return false;
3017
- }
3018
- }
3019
- function trackedFiles(root, isGit) {
3020
- if (!isGit) return /* @__PURE__ */ new Set();
3021
- const result = git(root, ["ls-files", "-z"]);
3022
- if (result.status !== 0) throw new Error("Could not enumerate tracked files.");
3023
- return new Set(result.stdout.split("\0").filter(Boolean).map(posixPath));
3024
- }
3025
- async function loadIgnore(root) {
3026
- const hardMatcher = (0, import_ignore.default)().add(DEFAULT_IGNORES);
3027
- const matcher = (0, import_ignore.default)().add(DEFAULT_IGNORES);
3028
- for (const name of [".gitignore", ".hlixignore"]) {
3029
- try {
3030
- const path = join4(root, name);
3031
- const metadata = await lstat(path);
3032
- if (metadata.isFile()) matcher.add(await readFile2(path, "utf8"));
3033
- } catch (error) {
3034
- if (error.code !== "ENOENT") throw error;
3035
- }
3036
- }
3037
- return { hardMatcher, matcher };
3038
- }
3039
- function resourceKind(path) {
3040
- if (MCP_FILE.test(path)) return "mcp";
3041
- if (SKILL_RESOURCE.test(path)) return "skill";
3042
- if (AGENT_RESOURCE.test(path)) return "agent";
3043
- return null;
3044
- }
3045
- function secretKind(path) {
3046
- if (DOTENV_FILE.test(path) && !SAFE_ENV_EXAMPLE.test(path)) return "dotenv";
3047
- if (CREDENTIAL_FILE.test(path)) return "credential";
3048
- return null;
3049
- }
3050
- function inferStack(paths) {
3051
- if (paths.has("package.json")) return "typescript";
3052
- if (paths.has("pyproject.toml") || paths.has("requirements.txt")) return "python";
3053
- if (paths.has("go.mod")) return "go";
3054
- if (paths.has("Cargo.toml")) return "rust";
3055
- if (paths.has("composer.json")) return "php";
3056
- if (paths.has("Gemfile")) return "ruby";
3057
- return "other";
3058
- }
3059
- function activeDotenvFiles(paths, explicit) {
3060
- if (explicit) return [posixPath(explicit)];
3061
- const priority = /* @__PURE__ */ new Map([
3062
- [".env", 0],
3063
- [".env.development", 1],
3064
- [".env.local", 2],
3065
- [".env.development.local", 3]
3066
- ]);
3067
- const active = paths.filter((path) => priority.has(basename(path).toLocaleLowerCase("en-US"))).sort((a, b) => {
3068
- const directory = a.slice(0, -basename(a).length).localeCompare(b.slice(0, -basename(b).length));
3069
- return directory || priority.get(basename(a).toLocaleLowerCase("en-US")) - priority.get(basename(b).toLocaleLowerCase("en-US"));
3070
- });
3071
- return active.length > 0 ? active : paths.length === 1 ? [...paths] : [];
3072
- }
3073
- function parseMcp(path, content) {
3074
- try {
3075
- const parsed = JSON.parse(content);
3076
- if (!parsed.mcpServers || typeof parsed.mcpServers !== "object") return [];
3077
- return Object.entries(parsed.mcpServers).map(([name, raw]) => {
3078
- const server = raw && typeof raw === "object" ? raw : {};
3079
- const transport = server.type === "http" || server.type === "sse" || server.type === "stdio" ? server.type : typeof server.command === "string" ? "stdio" : typeof server.url === "string" ? "http" : "unknown";
3080
- const env = server.env && typeof server.env === "object" ? server.env : {};
3081
- return {
3082
- sourcePath: path,
3083
- name,
3084
- transport,
3085
- ...typeof server.command === "string" ? { command: server.command } : {},
3086
- ...Array.isArray(server.args) && server.args.every((arg) => typeof arg === "string") ? { args: server.args } : {},
3087
- ...typeof server.url === "string" ? { url: server.url } : {},
3088
- envKeys: Object.keys(env).sort(),
3089
- enabled: false
3090
- };
3091
- });
3092
- } catch {
3093
- return [];
3094
- }
3095
- }
3096
- function parseHistoryOutput(output2, reason) {
3097
- const findings = [];
3098
- let commit = "unknown";
3099
- for (const line of output2.split(/\r?\n/)) {
3100
- if (line.startsWith("@@")) commit = line.slice(2);
3101
- else if (line.trim()) findings.push({ commit, path: posixPath(line.trim()), reason });
3102
- if (findings.length >= 100) break;
3103
- }
3104
- return findings;
3105
- }
3106
- function scanHistory(root, isGit) {
3107
- if (!isGit) {
3108
- return { head: null, branch: null, refs: [], commitCount: 0, complete: true, findings: [] };
3109
- }
3110
- const head = git(root, ["rev-parse", "HEAD"]).stdout.trim() || null;
3111
- const branchResult = git(root, ["symbolic-ref", "--short", "-q", "HEAD"]);
3112
- const branch = branchResult.status === 0 ? branchResult.stdout.trim() || null : null;
3113
- const count = Number.parseInt(git(root, ["rev-list", "--count", "--all"]).stdout.trim(), 10) || 0;
3114
- const refsResult = git(root, [
3115
- "for-each-ref",
3116
- "--format=%(refname)%00%(objectname)",
3117
- "refs/heads",
3118
- "refs/tags"
3119
- ]);
3120
- const refs = refsResult.stdout.split("\n").filter(Boolean).map((line) => {
3121
- const [name, oid] = line.split("\0");
3122
- return { name, oid };
3123
- }).filter(({ name, oid }) => /^refs\/(?:heads|tags)\/.+/.test(name) && /^[0-9a-f]{40,64}$/.test(oid)).sort((a, b) => a.name.localeCompare(b.name));
3124
- const common = ["log", "--all", `--max-count=${HISTORY_COMMIT_LIMIT}`, "--pretty=format:@@%H", "--name-only"];
3125
- const paths = git(root, [...common, "--", ".env", ".env.*", "*.pem", "*.key", ".npmrc", "*.tfvars"]);
3126
- const patterns = git(root, [
3127
- "log",
3128
- "--all",
3129
- `--max-count=${HISTORY_COMMIT_LIMIT}`,
3130
- "-G",
3131
- "BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|npm_[A-Za-z0-9]{30,}|gh[opusr]_[A-Za-z0-9_]{30,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{20,}",
3132
- "--pretty=format:@@%H",
3133
- "--name-only"
3134
- ]);
3135
- const dedupe = /* @__PURE__ */ new Map();
3136
- for (const finding of [
3137
- ...parseHistoryOutput(paths.stdout, "sensitive-path"),
3138
- ...parseHistoryOutput(patterns.stdout, "secret-pattern")
3139
- ]) {
3140
- dedupe.set(`${finding.commit}:${finding.path}:${finding.reason}`, finding);
3141
- }
3142
- return {
3143
- head,
3144
- branch,
3145
- refs,
3146
- commitCount: count,
3147
- complete: count <= HISTORY_COMMIT_LIMIT && refs.length <= 1e3 && refsResult.status === 0 && paths.status === 0 && patterns.status === 0,
3148
- findings: [...dedupe.values()].slice(0, 100)
3149
- };
3150
- }
3151
- async function scanProject(selectedRoot, options = {}) {
3152
- const root = await realpath(resolve4(selectedRoot));
3153
- if (!(await stat(root)).isDirectory()) throw new Error("Import target must be a directory.");
3154
- const isGit = await isSelectedGitRoot(root);
3155
- const tracked = trackedFiles(root, isGit);
3156
- const { hardMatcher, matcher } = await loadIgnore(root);
3157
- const files = [];
3158
- const secretFiles = [];
3159
- const resources = [];
3160
- const mcpServers = [];
3161
- const skipped = [];
3162
- const blocking = [];
3163
- const selectedEnvPath = options.envFile ? posixPath(relative(root, resolve4(root, options.envFile))) : void 0;
3164
- const selectedEnvIsContained = selectedEnvPath !== void 0 && selectedEnvPath !== "" && selectedEnvPath !== ".." && !selectedEnvPath.startsWith("../");
3165
- if (options.envFile && !selectedEnvIsContained) {
3166
- blocking.push(`Selected environment file must stay inside the project: ${options.envFile}`);
3167
- }
3168
- const dotenvValues = /* @__PURE__ */ new Map();
3169
- const envrcValues = /* @__PURE__ */ new Map();
3170
- const envrcKeysByPath = /* @__PURE__ */ new Map();
3171
- const envrcDynamicKeysByPath = /* @__PURE__ */ new Map();
3172
- const protectedConfigWarnings = [];
3173
- const protectedFiles = [];
3174
- const readableText = /* @__PURE__ */ new Map();
3175
- const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
3176
- const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
3177
- const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
3178
- let totalBytes = 0;
3179
- let protectedBytes = 0;
3180
- const visit = async (directory) => {
3181
- const entries = await readdir(directory, { withFileTypes: true });
3182
- entries.sort((a, b) => a.name.localeCompare(b.name));
3183
- for (const entry of entries) {
3184
- const absolute = join4(directory, entry.name);
3185
- const path = posixPath(relative(root, absolute));
3186
- if (entry.name === ".git") {
3187
- if (path !== ".git") blocking.push(`Nested Git repository is not supported: ${path}`);
3188
- continue;
3189
- }
3190
- const metadata = await lstat(absolute);
3191
- const ignoredPath = entry.isDirectory() ? `${path}/` : path;
3192
- const isTracked = tracked.has(path);
3193
- const ignored = !isTracked && matcher.ignores(ignoredPath);
3194
- const hardIgnored = !isTracked && hardMatcher.ignores(ignoredPath);
3195
- if (metadata.isSymbolicLink()) {
3196
- skipped.push({ path, reason: "symlink" });
3197
- if (!ignored) blocking.push(`Symlinks are not imported in this release: ${path}`);
3198
- continue;
3199
- }
3200
- if (entry.isDirectory()) {
3201
- if (hardIgnored) continue;
3202
- await visit(absolute);
3203
- continue;
3204
- }
3205
- const resource = resourceKind(path);
3206
- const kind = secretKind(path);
3207
- if (ignored && !resource && !kind) continue;
3208
- if (!entry.isFile()) {
3209
- skipped.push({ path, reason: "non-regular-file" });
3210
- continue;
3211
- }
3212
- if (files.length + secretFiles.length + resources.length >= maxFiles) {
3213
- blocking.push(`Project exceeds the ${maxFiles} file limit.`);
3214
- return;
3215
- }
3216
- if (metadata.size > maxFileBytes) {
3217
- blocking.push(`File exceeds the ${maxFileBytes} byte limit: ${path}`);
3218
- continue;
3219
- }
3220
- totalBytes += metadata.size;
3221
- if (totalBytes > maxTotalBytes) {
3222
- blocking.push(`Project exceeds the ${maxTotalBytes} byte total limit.`);
3223
- return;
3224
- }
3225
- const digest = await sha256(absolute);
3226
- if (resource) {
3227
- if (metadata.size > MAX_PROTECTED_FILE_BYTES) {
3228
- blocking.push(`Protected configuration exceeds the ${MAX_PROTECTED_FILE_BYTES} byte limit: ${path}`);
3229
- continue;
3230
- }
3231
- const content = await readFile2(absolute);
3232
- protectedBytes += content.byteLength;
3233
- if (protectedBytes > MAX_PROTECTED_TOTAL_BYTES) {
3234
- blocking.push(`Protected project files exceed the ${MAX_PROTECTED_TOTAL_BYTES} byte total limit.`);
3235
- continue;
3236
- }
3237
- resources.push({ path, kind: resource, size: metadata.size, sha256: digest });
3238
- protectedFiles.push({ path, kind: resource, contentBase64: content.toString("base64") });
3239
- if (resource === "mcp") {
3240
- mcpServers.push(...parseMcp(path, content.toString("utf8")));
3241
- }
3242
- continue;
3243
- }
3244
- if (kind) {
3245
- if (metadata.size > MAX_PROTECTED_FILE_BYTES) {
3246
- blocking.push(`Protected file exceeds the ${MAX_PROTECTED_FILE_BYTES} byte limit: ${path}`);
3247
- continue;
3248
- }
3249
- const content = await readFile2(absolute);
3250
- protectedBytes += content.byteLength;
3251
- if (protectedBytes > MAX_PROTECTED_TOTAL_BYTES) {
3252
- blocking.push(`Protected project files exceed the ${MAX_PROTECTED_TOTAL_BYTES} byte total limit.`);
3253
- continue;
3254
- }
3255
- let keys = [];
3256
- if (kind === "dotenv") {
3257
- try {
3258
- const values = parseDotenv(content);
3259
- const importableValues = {};
3260
- for (const [key, value] of Object.entries(values)) {
3261
- if (RESERVED_CONTROL_ENV.test(key)) {
3262
- blocking.push(`Hlix control-plane environment keys cannot be imported into a project: ${key}`);
3263
- continue;
3264
- }
3265
- importableValues[key] = value;
3266
- }
3267
- dotenvValues.set(path, importableValues);
3268
- keys = Object.keys(importableValues).sort();
3269
- } catch {
3270
- blocking.push(`Could not parse environment file: ${path}`);
3271
- }
3272
- } else if (basename(path) === ".envrc") {
3273
- const parsed = parseEnvrcLiterals(content);
3274
- const importableValues = {};
3275
- const importableKeys = /* @__PURE__ */ new Set();
3276
- const importableDynamicKeys = [];
3277
- for (const [key, value] of Object.entries(parsed.values)) {
3278
- if (RESERVED_CONTROL_ENV.test(key)) {
3279
- blocking.push(`Hlix control-plane environment keys cannot be imported into a project: ${key}`);
3280
- continue;
3281
- }
3282
- if (AMBIENT_RUNTIME_ENV.has(key)) {
3283
- protectedConfigWarnings.push(`Ignored ambient runtime assignment in ${path}: ${key}`);
3284
- continue;
3285
- }
3286
- importableKeys.add(key);
3287
- importableValues[key] = value;
3288
- }
3289
- envrcValues.set(path, importableValues);
3290
- for (const key of parsed.dynamicKeys) {
3291
- if (RESERVED_CONTROL_ENV.test(key)) {
3292
- blocking.push(`Hlix control-plane environment keys cannot be imported into a project: ${key}`);
3293
- continue;
3294
- }
3295
- if (AMBIENT_RUNTIME_ENV.has(key)) {
3296
- protectedConfigWarnings.push(`Ignored ambient runtime assignment in ${path}: ${key}`);
3297
- continue;
3298
- }
3299
- importableKeys.add(key);
3300
- importableDynamicKeys.push(key);
3301
- }
3302
- envrcKeysByPath.set(path, [...importableKeys].sort());
3303
- envrcDynamicKeysByPath.set(path, importableDynamicKeys.sort());
3304
- keys = [...importableKeys].sort();
3305
- if (importableDynamicKeys.length > 0) {
3306
- protectedConfigWarnings.push(
3307
- `Did not execute dynamic .envrc assignments; collect their resolved local values when available: ${importableDynamicKeys.join(", ")}`
3308
- );
3309
- }
3310
- }
3311
- secretFiles.push({ path, kind, keys, size: metadata.size, sha256: digest });
3312
- protectedFiles.push({ path, kind, contentBase64: content.toString("base64") });
3313
- continue;
3314
- }
3315
- if (metadata.size <= 1024 * 1024) {
3316
- const content = await readFile2(absolute);
3317
- const preview = content.toString("utf8");
3318
- readableText.set(path, preview);
3319
- if (HIGH_CONFIDENCE_SECRET.test(preview) || STRUCTURED_CONFIG.test(path) && STRUCTURED_LITERAL_SECRET.test(preview)) {
3320
- protectedBytes += content.byteLength;
3321
- if (protectedBytes > MAX_PROTECTED_TOTAL_BYTES) {
3322
- blocking.push(`Protected project files exceed the ${MAX_PROTECTED_TOTAL_BYTES} byte total limit.`);
3323
- continue;
3324
- }
3325
- secretFiles.push({ path, kind: "detected-content", keys: [], size: metadata.size, sha256: digest });
3326
- protectedFiles.push({ path, kind: "detected-content", contentBase64: content.toString("base64") });
3327
- continue;
3328
- }
3329
- }
3330
- files.push({
3331
- path,
3332
- size: metadata.size,
3333
- sha256: digest,
3334
- tracked: isTracked,
3335
- executable: (metadata.mode & 73) !== 0
3336
- });
3337
- }
3338
- };
3339
- await visit(root);
3340
- files.sort((a, b) => a.path.localeCompare(b.path));
3341
- secretFiles.sort((a, b) => a.path.localeCompare(b.path));
3342
- resources.sort((a, b) => a.path.localeCompare(b.path));
3343
- mcpServers.sort((a, b) => `${a.sourcePath}:${a.name}`.localeCompare(`${b.sourcePath}:${b.name}`));
3344
- const activeEnvFiles = activeDotenvFiles(
3345
- [...dotenvValues.keys()].sort(),
3346
- selectedEnvIsContained ? selectedEnvPath : void 0
3347
- );
3348
- if (options.envFile && selectedEnvIsContained && !dotenvValues.has(activeEnvFiles[0])) {
3349
- blocking.push(`Selected environment file was not found or is not a dotenv file: ${options.envFile}`);
3350
- }
3351
- const byKey = /* @__PURE__ */ new Map();
3352
- for (const [path, values] of dotenvValues) {
3353
- for (const key of Object.keys(values)) byKey.set(key, [...byKey.get(key) ?? [], path]);
3354
- }
3355
- for (const [path, keys] of envrcKeysByPath) {
3356
- for (const key of keys) byKey.set(key, [...byKey.get(key) ?? [], path]);
3357
- }
3358
- const envCollisions = [...byKey].filter(([, paths]) => paths.length > 1).map(([key, paths]) => ({ key, paths: paths.sort() })).sort((a, b) => a.key.localeCompare(b.key));
3359
- const secretValues = {};
3360
- for (const path of activeEnvFiles) Object.assign(secretValues, dotenvValues.get(path) ?? {});
3361
- const orderedEnvrcFiles = [...envrcValues.keys()].sort((a, b) => {
3362
- if (a === ".envrc") return 1;
3363
- if (b === ".envrc") return -1;
3364
- return a.localeCompare(b);
3365
- });
3366
- const winningEnvrcDynamicKeys = /* @__PURE__ */ new Set();
3367
- for (const path of orderedEnvrcFiles) {
3368
- const dynamicKeys = new Set(envrcDynamicKeysByPath.get(path) ?? []);
3369
- for (const key of envrcKeysByPath.get(path) ?? []) {
3370
- if (dynamicKeys.has(key)) {
3371
- delete secretValues[key];
3372
- winningEnvrcDynamicKeys.add(key);
3373
- continue;
3374
- }
3375
- const value = envrcValues.get(path)?.[key];
3376
- if (value !== void 0) secretValues[key] = value;
3377
- winningEnvrcDynamicKeys.delete(key);
3378
- }
3379
- }
3380
- const historyMode = options.historyMode ?? "abort_on_findings";
3381
- const history = scanHistory(root, isGit);
3382
- if (!history.complete && historyMode !== "preserve") {
3383
- blocking.push(`Git history scan exceeded ${HISTORY_COMMIT_LIMIT} commits or failed; import requires explicit --history preserve.`);
3384
- }
3385
- if (history.findings.length > 0 && historyMode === "abort_on_findings") {
3386
- blocking.push("Potential secrets exist in Git history. Abort, sanitize the repository, or explicitly use --history preserve.");
3387
- }
3388
- const pathSet = new Set(files.map((file) => file.path));
3389
- const discoveredEnvironment = await discoverEnvironment(root, pathSet, readableText);
3390
- const { observedSecretKeys, ...environment } = discoveredEnvironment;
3391
- environment.warnings.push(...protectedConfigWarnings);
3392
- const requiredSecretKeys = new Set(environment.requiredSecretKeys);
3393
- for (const key of winningEnvrcDynamicKeys) requiredSecretKeys.add(key);
3394
- for (const server of mcpServers) {
3395
- for (const key of server.envKeys) requiredSecretKeys.add(key);
3396
- }
3397
- environment.requiredSecretKeys = [...requiredSecretKeys].sort();
3398
- for (const key of environment.requiredSecretKeys) {
3399
- if (RESERVED_CONTROL_ENV.test(key)) {
3400
- blocking.push(`Hlix control-plane environment keys cannot be imported into a project: ${key}`);
3401
- continue;
3402
- }
3403
- if (secretValues[key] !== void 0) continue;
3404
- const value = options.environment?.[key];
3405
- if (value === void 0) {
3406
- blocking.push(`Required project environment value was not found locally: ${key}`);
3407
- continue;
3408
- }
3409
- secretValues[key] = value;
3410
- }
3411
- for (const key of observedSecretKeys) {
3412
- if (RESERVED_CONTROL_ENV.test(key)) {
3413
- environment.warnings.push(`Ignored reserved Hlix control-plane environment reference: ${key}`);
3414
- continue;
3415
- }
3416
- if (AMBIENT_RUNTIME_ENV.has(key)) continue;
3417
- if (secretValues[key] !== void 0) continue;
3418
- const value = options.environment?.[key];
3419
- if (value === void 0) {
3420
- environment.warnings.push(`Observed environment reference has no local value: ${key}`);
3421
- continue;
3422
- }
3423
- secretValues[key] = value;
3424
- }
3425
- environment.warnings = [...new Set(environment.warnings)].sort();
3426
- const manifest = {
3427
- schemaVersion: PROJECT_MANIFEST_VERSION,
3428
- rootName: options.rootName ?? basename(root),
3429
- stack: inferStack(pathSet),
3430
- git: {
3431
- detected: isGit,
3432
- head: history.head,
3433
- branch: history.branch,
3434
- refs: history.refs,
3435
- commitCount: history.commitCount,
3436
- historyScanComplete: history.complete,
3437
- historyMode
3438
- },
3439
- files,
3440
- secretFiles,
3441
- activeEnvFiles,
3442
- secretKeys: Object.keys(secretValues).sort(),
3443
- envCollisions,
3444
- quarantinedResources: resources,
3445
- mcpServers,
3446
- historicalSecretFindings: history.findings,
3447
- skipped,
3448
- environment,
3449
- totals: { files: files.length, bytes: files.reduce((sum, file) => sum + file.size, 0) }
3450
- };
3451
- const manifestJson = stable(manifest);
3452
- const manifestSha256 = createHash("sha256").update(manifestJson).digest("hex");
3453
- return {
3454
- root,
3455
- manifest,
3456
- manifestJson,
3457
- manifestSha256,
3458
- secretValues,
3459
- protectedFiles: protectedFiles.sort((a, b) => a.path.localeCompare(b.path)),
3460
- blocking: [...new Set(blocking)]
3461
- };
3462
- }
3463
-
3464
- // src/commands/init.ts
3465
- var initProject = {
3466
- name: "init",
3467
- summary: "Configure a local folder for Hlix",
3468
- usage: [
3469
- "Usage: hlix init [folder] [--json]",
3470
- "",
3471
- "Creates .hlix/config.json, .hlix/.gitignore, and .hlixignore.",
3472
- "Project configuration is safe to commit; local revision state is ignored."
3473
- ].join("\n"),
3474
- supportsJson: true,
3475
- args: { min: 0, max: 1 },
3476
- async run(ctx) {
3477
- if (ctx.args.length > 1) throw new Error("init accepts at most one folder.");
3478
- const root = await realpath2(resolve5(ctx.cwd, ctx.args[0] ?? "."));
3479
- const target = resolveTarget({ env: ctx.env, flags: ctx.flags, cwd: root });
3480
- if (target.conflict) throw new CliError("workspace_mismatch", target.conflict);
3481
- if (!target.workspaceId) {
3482
- throw new Error("No workspace selected. Run `hlix auth login --workspace <id>` first.");
3483
- }
3484
- await ctx.requireClient(root).projects.list();
3485
- const existing = readProjectConfig(root);
3486
- const config = existing ?? {
3487
- schemaVersion: 1,
3488
- workspaceId: target.workspaceId,
3489
- apiUrl: target.baseUrl
3490
- };
3491
- writeProjectConfig(root, config);
3492
- const scan = await scanProject(root, { environment: ctx.env });
3493
- const data = {
3494
- root,
3495
- config,
3496
- scan: {
3497
- files: scan.manifest.totals.files,
3498
- bytes: scan.manifest.totals.bytes,
3499
- secretFiles: scan.manifest.secretFiles.map((file) => file.path),
3500
- quarantinedResources: scan.manifest.quarantinedResources.map((item) => item.path),
3501
- blocking: scan.blocking
3502
- }
3503
- };
3504
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(initProject.name, data)));
3505
- else {
3506
- ctx.stdout(
3507
- `Initialized ${root}
3508
- Project: ${config.projectId ?? "not imported yet"}
3509
- Workspace: ${config.workspaceId}
3510
- `
3511
- );
3512
- if (scan.blocking.length) {
3513
- ctx.stdout(`
3514
- Review before import:
3515
- ${scan.blocking.map((item) => `- ${item}`).join("\n")}
3516
- `);
3517
- }
3518
- }
3519
- return 0;
3520
- }
3521
- };
3522
-
3523
- // src/commands/import.ts
3524
- import { createHash as createHash3 } from "crypto";
3525
- import { readFile as readFile3, realpath as realpath3 } from "fs/promises";
3526
- import { basename as basename2, resolve as resolve6 } from "path";
3527
-
3528
- // src/project/snapshot.ts
3529
- import { createHash as createHash2 } from "crypto";
3530
- import { createReadStream as createReadStream2 } from "fs";
3531
- import {
3532
- chmod,
3533
- copyFile,
3534
- lstat as lstat2,
3535
- mkdir,
3536
- mkdtemp,
3537
- readdir as readdir2,
3538
- rm
3539
- } from "fs/promises";
3540
- import { tmpdir } from "os";
3541
- import { dirname as dirname4, join as join5 } from "path";
3542
- import { spawnSync as spawnSync2 } from "child_process";
3543
- function run(cwd, args) {
3544
- const result = spawnSync2("git", ["-C", cwd, ...args], {
3545
- encoding: "utf8",
3546
- maxBuffer: 20 * 1024 * 1024,
3547
- stdio: ["ignore", "pipe", "pipe"]
3548
- });
3549
- if (result.status !== 0) {
3550
- const detail = result.stderr.trim().split("\n").at(-1) ?? "git command failed";
3551
- throw new Error(detail);
3552
- }
3553
- return result.stdout.trim();
3554
- }
3555
- async function hash(path) {
3556
- const digest = createHash2("sha256");
3557
- for await (const chunk of createReadStream2(path)) digest.update(chunk);
3558
- return digest.digest("hex");
3559
- }
3560
- async function emptyWorkingTree(repo) {
3561
- for (const entry of await readdir2(repo)) {
3562
- if (entry === ".git") continue;
3563
- await rm(join5(repo, entry), { recursive: true, force: true });
3564
- }
3565
- }
3566
- async function createProjectSnapshot(scan) {
3567
- if (scan.blocking.length > 0) {
3568
- throw new Error(`Project scan has blocking findings:
3569
- - ${scan.blocking.join("\n- ")}`);
3570
- }
3571
- const temporary = await mkdtemp(join5(tmpdir(), "hlix-snapshot-"));
3572
- await chmod(temporary, 448);
3573
- const repo = join5(temporary, "repo");
3574
- const mirror = join5(temporary, "source.git");
3575
- const bundlePath = join5(temporary, "project.bundle");
3576
- try {
3577
- if (scan.manifest.git.detected) {
3578
- const mirrorResult = spawnSync2("git", ["clone", "--mirror", "--no-hardlinks", scan.root, mirror], {
3579
- encoding: "utf8",
3580
- stdio: ["ignore", "pipe", "pipe"]
3581
- });
3582
- if (mirrorResult.status !== 0) throw new Error(mirrorResult.stderr.trim() || "Could not mirror repository.");
3583
- const mirroredRefs = new Map(
3584
- run(mirror, [
3585
- "for-each-ref",
3586
- "--format=%(refname)%00%(objectname)",
3587
- "refs/heads",
3588
- "refs/tags"
3589
- ]).split("\n").filter(Boolean).map((line) => line.split("\0", 2))
3590
- );
3591
- for (const ref of scan.manifest.git.refs) {
3592
- if (mirroredRefs.get(ref.name) !== ref.oid) throw new Error(`Git ref changed after scan: ${ref.name}`);
3593
- }
3594
- const cloneResult = spawnSync2("git", ["clone", "--no-hardlinks", mirror, repo], {
3595
- encoding: "utf8",
3596
- stdio: ["ignore", "pipe", "pipe"]
3597
- });
3598
- if (cloneResult.status !== 0) throw new Error(cloneResult.stderr.trim() || "Could not create snapshot clone.");
3599
- if (!scan.manifest.git.head) throw new Error("Git repository has no HEAD commit.");
3600
- run(repo, ["checkout", "-B", "main", scan.manifest.git.head]);
3601
- } else {
3602
- await mkdir(repo, { recursive: true, mode: 448 });
3603
- run(repo, ["init", "-b", "main"]);
3604
- }
3605
- await emptyWorkingTree(repo);
3606
- for (const file of scan.manifest.files) {
3607
- const source = join5(scan.root, ...file.path.split("/"));
3608
- const sourceEntry = await lstat2(source);
3609
- if (!sourceEntry.isFile() || sourceEntry.isSymbolicLink()) {
3610
- throw new Error(`Source changed after scan: ${file.path}`);
3611
- }
3612
- if (sourceEntry.size !== file.size || await hash(source) !== file.sha256) {
3613
- throw new Error(`Source changed after scan: ${file.path}`);
3614
- }
3615
- const destination = join5(repo, ...file.path.split("/"));
3616
- await mkdir(dirname4(destination), { recursive: true, mode: 493 });
3617
- await copyFile(source, destination);
3618
- await chmod(destination, file.executable ? 493 : 420);
3619
- }
3620
- run(repo, ["config", "user.name", "Hlix Import"]);
3621
- run(repo, ["config", "user.email", "import@hlix.ai"]);
3622
- run(repo, ["add", "-A"]);
3623
- run(repo, ["commit", "--allow-empty", "-m", "chore(hlix): import snapshot"]);
3624
- const commitSha = run(repo, ["rev-parse", "HEAD"]);
3625
- const refsToCreate = scan.manifest.git.refs.filter((ref) => ref.name !== "refs/heads/main");
3626
- if (refsToCreate.length > 0) {
3627
- const updateResult = spawnSync2("git", ["-C", repo, "update-ref", "--stdin"], {
3628
- encoding: "utf8",
3629
- input: refsToCreate.map((ref) => `update ${ref.name} ${ref.oid}`).join("\n") + "\n",
3630
- stdio: ["pipe", "pipe", "pipe"]
3631
- });
3632
- if (updateResult.status !== 0) {
3633
- throw new Error(updateResult.stderr.trim() || "Could not recreate the approved Git refs.");
3634
- }
3635
- }
3636
- const approvedRefs = refsToCreate.map((ref) => ref.name);
3637
- const bundleResult = spawnSync2("git", ["-C", repo, "bundle", "create", bundlePath, "--stdin"], {
3638
- encoding: "utf8",
3639
- input: ["refs/heads/main", ...approvedRefs].join("\n") + "\n",
3640
- stdio: ["pipe", "pipe", "pipe"]
3641
- });
3642
- if (bundleResult.status !== 0) {
3643
- throw new Error(bundleResult.stderr.trim() || "Could not create the approved Git bundle.");
3644
- }
3645
- run(repo, ["bundle", "verify", bundlePath]);
3646
- await chmod(bundlePath, 384);
3647
- const bundleSize = (await lstat2(bundlePath)).size;
3648
- const bundleSha256 = await hash(bundlePath);
3649
- return {
3650
- bundlePath,
3651
- bundleSha256,
3652
- bundleSize,
3653
- commitSha,
3654
- dispose: () => rm(temporary, { recursive: true, force: true })
3655
- };
3656
- } catch (error) {
3657
- await rm(temporary, { recursive: true, force: true });
3658
- throw error;
3659
- }
3660
- }
3661
-
3662
- // src/commands/import.ts
3663
- function flagString(flags, name) {
3664
- const value = flags[name];
3665
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
3666
- }
3667
- function idempotencyKey(manifestSha256, bundleSha256) {
3668
- return createHash3("sha256").update(`project-import-v1\0${manifestSha256}\0${bundleSha256}`).digest("hex");
3669
- }
3670
- function trustPrompt(scan) {
3671
- const setup = scan.manifest.environment.install;
3672
- const protectedCount = scan.manifest.secretFiles.length + scan.manifest.quarantinedResources.length;
3673
- if (setup.length === 0 && protectedCount === 0 && scan.manifest.secretKeys.length === 0) return null;
3674
- const lines = [
3675
- "Hlix will upload this project and encrypted protected files to your selected workspace."
3676
- ];
3677
- if (setup.length > 0) {
3678
- lines.push("Detected cloud setup commands:", ...setup.map((command) => `- ${command}`));
3679
- }
3680
- if (scan.manifest.secretKeys.length > 0) {
3681
- lines.push(
3682
- "Project secret keys available to cloud tasks:",
3683
- ...scan.manifest.secretKeys.map((key) => `- ${key}`)
3684
- );
3685
- }
3686
- if (scan.manifest.quarantinedResources.length > 0) {
3687
- lines.push(
3688
- "Quarantined resources (preserved but not activated):",
3689
- ...scan.manifest.quarantinedResources.map((item) => `- ${item.kind}: ${item.path}`)
3690
- );
3691
- }
3692
- if (scan.manifest.environment.warnings.length > 0) {
3693
- lines.push(
3694
- "Environment warnings:",
3695
- ...scan.manifest.environment.warnings.map((warning) => `- ${warning}`)
3696
- );
3697
- }
3698
- lines.push("Setup runs inside the isolated Coding Workspace and may access project secrets.");
3699
- return lines.join("\n");
3700
- }
3701
- var importProject = {
3702
- name: "import",
3703
- summary: "Import a local project into your Hlix workspace",
3704
- usage: [
3705
- "Usage: hlix import [folder] [options]",
3706
- "",
3707
- "Options:",
3708
- " --dry-run Scan and report without uploading",
3709
- " --env-file <path> Override automatic development dotenv precedence",
3710
- " --history <mode> abort-on-findings (default) or preserve",
3711
- " --name <name> Cloud project name (defaults to folder name)",
3712
- " --stack <stack> Override detected stack",
3713
- " --yes Approve the reviewed upload and cloud setup",
3714
- " --json Machine-readable output (never contains secret values)"
3715
- ].join("\n"),
3716
- supportsJson: true,
3717
- options: ["dry-run", "env-file", "history", "name", "stack", "yes"],
3718
- args: { min: 0, max: 1 },
3719
- async run(ctx) {
3720
- if (ctx.args.length > 1) throw new Error("import accepts at most one folder.");
3721
- const root = await realpath3(resolve6(ctx.cwd, ctx.args[0] ?? "."));
3722
- const target = resolveTarget({ env: ctx.env, flags: ctx.flags, cwd: root });
3723
- if (target.conflict) throw new CliError("workspace_mismatch", target.conflict);
3724
- if (!target.workspaceId || !target.credential) {
3725
- throw new CliError(
3726
- "workspace_required",
3727
- "No workspace selected. Run `hlix auth login --workspace <id>` first."
3728
- );
3729
- }
3730
- const existing = readProjectConfig(root);
3731
- if (existing?.projectId) {
3732
- throw new CliError(
3733
- "already_imported",
3734
- "This folder is already bound to an Hlix project. Use `hlix push`."
3735
- );
3736
- }
3737
- const historyFlag = flagString(ctx.flags, "history");
3738
- if (historyFlag && historyFlag !== "preserve" && historyFlag !== "abort-on-findings") {
3739
- throw new Error("--history must be `abort-on-findings` or `preserve`.");
3740
- }
3741
- if (!existing && ctx.flags["dry-run"] !== true) {
3742
- writeProjectConfig(root, {
3743
- schemaVersion: 1,
3744
- workspaceId: target.workspaceId,
3745
- apiUrl: target.baseUrl
3746
- });
3747
- }
3748
- const scan = await scanProject(root, {
3749
- ...flagString(ctx.flags, "env-file") ? { envFile: flagString(ctx.flags, "env-file") } : {},
3750
- environment: ctx.env,
3751
- historyMode: historyFlag === "preserve" ? "preserve" : "abort_on_findings"
3752
- });
3753
- if (ctx.flags["dry-run"] !== true && scan.blocking.length === 0) {
3754
- const prompt = trustPrompt(scan);
3755
- if (prompt && ctx.flags.yes !== true) {
3756
- if (ctx.json || !ctx.confirm) {
3757
- throw new CliError(
3758
- "approval_required",
3759
- "Import needs approval for protected files or cloud setup. Review with `hlix import --dry-run`, then repeat with `--yes`."
3760
- );
3761
- }
3762
- if (!await ctx.confirm(prompt)) {
3763
- throw new CliError("cancelled", "Import cancelled.");
3764
- }
3765
- }
3766
- }
3767
- const report = {
3768
- root,
3769
- name: flagString(ctx.flags, "name") ?? basename2(root),
3770
- stack: flagString(ctx.flags, "stack") ?? scan.manifest.stack,
3771
- files: scan.manifest.totals.files,
3772
- bytes: scan.manifest.totals.bytes,
3773
- activeEnvFiles: scan.manifest.activeEnvFiles,
3774
- secretKeys: scan.manifest.secretKeys,
3775
- protectedFiles: scan.manifest.secretFiles.length + scan.manifest.quarantinedResources.length,
3776
- environment: scan.manifest.environment,
3777
- historicalSecretFindings: scan.manifest.historicalSecretFindings,
3778
- quarantinedResources: scan.manifest.quarantinedResources,
3779
- mcpServers: scan.manifest.mcpServers,
3780
- skipped: scan.manifest.skipped,
3781
- blocking: scan.blocking,
3782
- manifestSha256: scan.manifestSha256
3783
- };
3784
- if (ctx.flags["dry-run"] === true) {
3785
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(importProject.name, report)));
3786
- else {
3787
- const protectedPaths = [
3788
- ...scan.manifest.secretFiles.map((item) => `${item.kind}: ${item.path}`),
3789
- ...scan.manifest.quarantinedResources.map((item) => `${item.kind}: ${item.path}`)
3790
- ];
3791
- ctx.stdout(
3792
- [
3793
- `Project: ${report.name}`,
3794
- `Stack: ${report.stack}`,
3795
- `Files: ${report.files}`,
3796
- `Bytes: ${report.bytes}`,
3797
- `Environment confidence: ${report.environment.confidence}`,
3798
- `History findings: ${report.historicalSecretFindings.length}`,
3799
- "",
3800
- "Environment evidence:",
3801
- ...report.environment.sources.length ? report.environment.sources.map(
3802
- (source) => `- ${source.kind}: ${source.path}`
3803
- ) : ["- none detected"],
3804
- "",
3805
- "Cloud setup commands:",
3806
- ...report.environment.install.length ? report.environment.install.map((command) => `- ${command}`) : ["- none detected"],
3807
- "",
3808
- "Required secret keys:",
3809
- ...report.environment.requiredSecretKeys.length ? report.environment.requiredSecretKeys.map((key) => `- ${key}`) : ["- none detected"],
3810
- "",
3811
- "Collected secret keys:",
3812
- ...report.secretKeys.length ? report.secretKeys.map((key) => `- ${key}`) : ["- none detected"],
3813
- "",
3814
- "Protected and quarantined paths:",
3815
- ...protectedPaths.length ? protectedPaths.map((path) => `- ${path}`) : ["- none"],
3816
- "",
3817
- "Environment warnings:",
3818
- ...report.environment.warnings.length ? report.environment.warnings.map((warning) => `- ${warning}`) : ["- none"],
3819
- ...report.blocking.length ? ["", "Blocking findings:", ...report.blocking.map((item) => `- ${item}`)] : []
3820
- ].join("\n") + "\n"
3821
- );
3822
- }
3823
- return scan.blocking.length ? 1 : 0;
3824
- }
3825
- if (scan.blocking.length) {
3826
- throw new CliError(
3827
- "scan_blocked",
3828
- `Project scan blocked import:
3829
- - ${scan.blocking.join("\n- ")}`
3830
- );
3831
- }
3832
- const snapshot = await createProjectSnapshot(scan);
3833
- try {
3834
- const client = ctx.requireClient(root);
3835
- const created = await client.imports.create({
3836
- idempotencyKey: idempotencyKey(scan.manifestSha256, snapshot.bundleSha256),
3837
- name: report.name,
3838
- stack: report.stack,
3839
- defaultBranch: "main",
3840
- historyMode: scan.manifest.git.historyMode,
3841
- manifest: scan.manifest,
3842
- manifestSha256: scan.manifestSha256,
3843
- bundleSha256: snapshot.bundleSha256,
3844
- bundleSize: snapshot.bundleSize,
3845
- commitSha: snapshot.commitSha
3846
- });
3847
- if (created.import.status === "pending_upload") {
3848
- if (!created.upload) {
3849
- throw new Error(
3850
- `Import ${created.import.id} is ${created.import.status} but has no upload target.`
3851
- );
3852
- }
3853
- await client.imports.uploadBundle(
3854
- created.upload,
3855
- new Blob([Uint8Array.from(await readFile3(snapshot.bundlePath))]),
3856
- {
3857
- onProgress: (uploaded, total) => {
3858
- if (!ctx.json) ctx.stderr(`Uploading ${uploaded}/${total} bytes\r`);
3859
- }
3860
- }
3861
- );
3862
- }
3863
- const finalized = created.import.status === "ready" ? { import: created.import } : await client.imports.finalize(created.import.id, {
3864
- secrets: scan.secretValues,
3865
- protectedFiles: scan.protectedFiles
3866
- });
3867
- const imported = finalized.import;
3868
- const projectId = imported.projectId ?? imported.requestedProjectId;
3869
- if (!projectId || !imported.revisionId || !imported.generation) {
3870
- throw new Error("Import completed without project revision metadata.");
3871
- }
3872
- writeProjectConfig(root, {
3873
- schemaVersion: 1,
3874
- projectId,
3875
- workspaceId: target.workspaceId,
3876
- apiUrl: target.baseUrl
3877
- });
3878
- writeProjectState(root, {
3879
- schemaVersion: 1,
3880
- revisionId: imported.revisionId,
3881
- generation: imported.generation,
3882
- commitSha: snapshot.commitSha,
3883
- manifestSha256: scan.manifestSha256,
3884
- historyMode: scan.manifest.git.historyMode,
3885
- rootName: scan.manifest.rootName
3886
- });
3887
- const result = {
3888
- projectId,
3889
- revisionId: imported.revisionId,
3890
- generation: imported.generation,
3891
- commitSha: snapshot.commitSha,
3892
- files: scan.manifest.totals.files,
3893
- bytes: scan.manifest.totals.bytes
3894
- };
3895
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(importProject.name, result)));
3896
- else {
3897
- ctx.stderr("\n");
3898
- ctx.stdout(
3899
- `Imported ${report.name}
3900
- Project ID: ${projectId}
3901
- Revision: ${imported.generation}
3902
- `
3903
- );
3904
- }
3905
- return 0;
3906
- } finally {
3907
- await snapshot.dispose();
3908
- }
3909
- }
3910
- };
3911
-
3912
- // src/commands/status.ts
3913
- var messageOf2 = (error) => error instanceof Error ? error.message : String(error);
3914
- async function localState(ctx, root, historyMode, rootName, base) {
3915
- try {
3916
- const scan = await scanProject(root, { historyMode, environment: ctx.env, rootName });
3917
- return {
3918
- files: scan.manifest.totals.files,
3919
- manifestSha256: scan.manifestSha256,
3920
- changed: base ? scan.manifestSha256 !== base : null,
3921
- blocking: scan.blocking,
3922
- error: null
3923
- };
3924
- } catch (error) {
3925
- return { files: null, manifestSha256: null, changed: null, blocking: [], error: messageOf2(error) };
3926
- }
3927
- }
3928
- var statusCommand = {
3929
- name: "status",
3930
- summary: "Show the workspace, folder binding, and local-vs-cloud drift",
3931
- usage: [
3932
- "Usage: hlix status [--cwd <dir>] [--json]",
3933
- "",
3934
- "Reports the resolved workspace and API with the source that decided each,",
3935
- "whether this folder is bound to a project, and whether the working tree or",
3936
- "the cloud head has moved since the last sync. Every finding is a line in",
3937
- "the output, so the command itself always exits 0; only an argument error",
3938
- "(rejected before it runs) exits 2."
3939
- ].join("\n"),
3940
- supportsJson: true,
3941
- async run(ctx) {
3942
- const target = resolveTarget({ env: ctx.env, flags: ctx.flags, cwd: ctx.cwd });
3943
- const binding2 = target.binding;
3944
- const state = binding2 ? readProjectState(binding2.root) : null;
3945
- const local = binding2 ? await localState(
3946
- ctx,
3947
- binding2.root,
3948
- state?.historyMode ?? "preserve",
3949
- state?.rootName,
3950
- state?.manifestSha256
3951
- ) : null;
3952
- let cloud = null;
3953
- if (binding2?.config.projectId && target.credential && !target.conflict) {
3954
- try {
3955
- const head = (await ctx.requireClient().revisions.head(binding2.config.projectId)).revision;
3956
- cloud = {
3957
- revisionId: head.id,
3958
- generation: head.generation,
3959
- changed: state ? head.id !== state.revisionId || head.generation !== state.generation : null,
3960
- error: null
3961
- };
3962
- } catch (error) {
3963
- cloud = { revisionId: "", generation: 0, changed: null, error: errorCode(error).code };
3964
- }
3965
- }
3966
- const data = {
3967
- cwd: ctx.cwd,
3968
- workspace: { id: target.workspaceId, source: target.workspaceSource },
3969
- api: { baseUrl: target.baseUrl, source: target.baseUrlSource },
3970
- credential: {
3971
- present: target.credential !== null,
3972
- source: target.credentialSource,
3973
- path: target.credentialPath
3974
- },
3975
- conflict: target.conflict,
3976
- project: binding2 ? {
3977
- root: binding2.root,
3978
- projectId: binding2.config.projectId ?? null,
3979
- workspaceId: binding2.config.workspaceId,
3980
- apiUrl: binding2.config.apiUrl
3981
- } : null,
3982
- revision: state ? { id: state.revisionId, generation: state.generation, commitSha: state.commitSha } : null,
3983
- local,
3984
- cloud
3985
- };
3986
- if (ctx.json) {
3987
- ctx.stdout(renderJson(jsonSuccess(statusCommand.name, data)));
3988
- return 0;
3989
- }
3990
- const fields = [
3991
- ["folder", ctx.cwd],
3992
- ["workspace", target.workspaceId ? `${target.workspaceId} (from ${target.workspaceSource})` : "none \u2014 run `hlix auth login --workspace <id>`"],
3993
- ["api", `${target.baseUrl} (from ${target.baseUrlSource})`],
3994
- [
3995
- "credential",
3996
- target.credential ? target.credentialSource === "env" ? "environment (HLIX_API_KEY)" : `file (${target.credentialPath})` : "none \u2014 run `hlix auth login`"
3997
- ],
3998
- [
3999
- "project",
4000
- binding2 ? binding2.config.projectId ?? `initialized at ${binding2.root}, not imported yet` : "none \u2014 run `hlix import .` to bind this folder"
4001
- ]
4002
- ];
4003
- if (state) fields.push(["revision", `generation ${state.generation} (${state.revisionId})`]);
4004
- if (local) {
4005
- fields.push([
4006
- "local",
4007
- local.error ? `could not scan: ${local.error}` : local.changed === null ? `${local.files} files, no synced base revision` : local.changed ? `${local.files} files, modified since the last sync` : `${local.files} files, matches the last sync`
4008
- ]);
4009
- if (local.blocking.length > 0) {
4010
- fields.push(["blocking", local.blocking.join("; ")]);
4011
- }
4012
- }
4013
- if (cloud) {
4014
- fields.push([
4015
- "cloud",
4016
- cloud.error ? `not checked: ${cloud.error}` : cloud.changed ? `generation ${cloud.generation} available \u2014 run \`hlix pull\`` : `generation ${cloud.generation}, up to date`
4017
- ]);
4018
- }
4019
- if (target.conflict) fields.push(["conflict", target.conflict]);
4020
- ctx.stdout(`${renderFields(fields)}
4021
- `);
4022
- return 0;
4023
- }
4024
- };
4025
-
4026
- // src/commands/sync.ts
4027
- import { createHash as createHash5 } from "crypto";
4028
- import { readFile as readFile4, realpath as realpath5 } from "fs/promises";
4029
-
4030
- // src/project/apply.ts
4031
- import { createHash as createHash4 } from "crypto";
4032
- import { constants as constants3 } from "fs";
4033
- import {
4034
- chmod as chmod2,
4035
- copyFile as copyFile2,
4036
- lstat as lstat3,
4037
- mkdir as mkdir2,
4038
- mkdtemp as mkdtemp2,
4039
- open,
4040
- realpath as realpath4,
4041
- rename,
4042
- rm as rm2,
4043
- writeFile
4044
- } from "fs/promises";
4045
- import { tmpdir as tmpdir2 } from "os";
4046
- import { dirname as dirname5, join as join6, relative as relative2, resolve as resolve7, sep as sep2 } from "path";
4047
- import { spawnSync as spawnSync3 } from "child_process";
4048
- function manifestIdentity(manifest) {
4049
- let value = manifest;
4050
- if (typeof value === "string") {
4051
- try {
4052
- value = JSON.parse(value);
4053
- } catch {
4054
- return {};
4055
- }
4056
- }
4057
- if (!value || typeof value !== "object") return {};
4058
- const record = value;
4059
- const git3 = record.git && typeof record.git === "object" ? record.git : {};
4060
- const historyMode = git3.historyMode === "preserve" || git3.historyMode === "abort_on_findings" ? git3.historyMode : void 0;
4061
- const rootName = typeof record.rootName === "string" && record.rootName.trim() ? record.rootName : void 0;
4062
- return { historyMode, rootName };
4063
- }
4064
- function manifestFiles(manifest) {
4065
- let value = manifest;
4066
- if (typeof value === "string") {
4067
- try {
4068
- value = JSON.parse(value);
4069
- } catch {
4070
- return [];
4071
- }
4072
- }
4073
- if (!value || typeof value !== "object") return [];
4074
- const files = value.files;
4075
- if (!Array.isArray(files)) return [];
4076
- return files.flatMap((entry) => {
4077
- if (!entry || typeof entry !== "object") return [];
4078
- const record = entry;
4079
- return typeof record.path === "string" && typeof record.sha256 === "string" ? [{ path: record.path, sha256: record.sha256 }] : [];
4080
- });
4081
- }
4082
- function git2(root, args) {
4083
- const result = spawnSync3("git", ["-C", root, ...args], {
4084
- encoding: "utf8",
4085
- maxBuffer: 50 * 1024 * 1024,
4086
- stdio: ["ignore", "pipe", "pipe"]
4087
- });
4088
- if (result.status !== 0) throw new Error(result.stderr.trim().split("\n").at(-1) ?? "Git command failed");
4089
- return result.stdout.trim();
4090
- }
4091
- function sha2562(bytes) {
4092
- return createHash4("sha256").update(bytes).digest("hex");
4093
- }
4094
- async function assertSafeParent(root, path) {
4095
- const absoluteRoot = await realpath4(root);
4096
- const target = resolve7(absoluteRoot, ...path.split("/"));
4097
- if (target !== absoluteRoot && !target.startsWith(absoluteRoot + sep2)) throw new Error(`Protected path escapes the project: ${path}`);
4098
- const rel = relative2(absoluteRoot, dirname5(target));
4099
- let current = absoluteRoot;
4100
- for (const part of rel.split(sep2).filter(Boolean)) {
4101
- current = join6(current, part);
4102
- try {
4103
- const entry = await lstat3(current);
4104
- if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error(`Protected path crosses a non-directory: ${path}`);
4105
- } catch (error) {
4106
- if (error.code !== "ENOENT") throw error;
4107
- await mkdir2(current, { mode: 448 });
4108
- }
4109
- }
4110
- return target;
4111
- }
4112
- async function writeProtected(root, path, bytes) {
4113
- const target = await assertSafeParent(root, path);
4114
- try {
4115
- const existing = await lstat3(target);
4116
- if (!existing.isFile() || existing.isSymbolicLink()) throw new Error(`Refusing to replace non-regular protected file: ${path}`);
4117
- } catch (error) {
4118
- if (error.code !== "ENOENT") throw error;
4119
- }
4120
- const temporary = join6(dirname5(target), `.hlix-pull-${crypto.randomUUID()}.tmp`);
4121
- const handle = await open(
4122
- temporary,
4123
- constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW,
4124
- 384
4125
- );
4126
- try {
4127
- await handle.writeFile(bytes);
4128
- await handle.sync();
4129
- } finally {
4130
- await handle.close();
4131
- }
4132
- try {
4133
- await rename(temporary, target);
4134
- } catch (error) {
4135
- await rm2(temporary, { force: true });
4136
- throw error;
4137
- }
4138
- }
4139
- async function preflightReplacement(root, path) {
4140
- const target = await assertSafeParent(root, path);
4141
- try {
4142
- const existing = await lstat3(target);
4143
- if (!existing.isFile() || existing.isSymbolicLink()) {
4144
- throw new Error(`Refusing to replace non-regular project file: ${path}`);
4145
- }
4146
- } catch (error) {
4147
- if (error.code !== "ENOENT") throw error;
4148
- }
4149
- }
4150
- async function applyPull(root, scan, head, bundleBlob, protectedResponse) {
4151
- const temporary = await mkdtemp2(join6(tmpdir2(), "hlix-pull-"));
4152
- await chmod2(temporary, 448);
4153
- const bundlePath = join6(temporary, "project.bundle");
4154
- const checkout = join6(temporary, "checkout");
4155
- try {
4156
- const bundle = Buffer.from(await bundleBlob.arrayBuffer());
4157
- if (bundle.byteLength !== head.bundleSize || sha2562(bundle) !== head.bundleSha256) {
4158
- throw new Error("Downloaded revision bundle failed integrity verification.");
4159
- }
4160
- await writeFile(bundlePath, bundle, { mode: 384 });
4161
- git2(temporary, ["clone", "--no-hardlinks", bundlePath, checkout]);
4162
- git2(checkout, ["bundle", "verify", bundlePath]);
4163
- if (git2(checkout, ["rev-parse", "HEAD"]) !== head.commitSha) {
4164
- throw new Error("Downloaded revision bundle does not match the cloud commit.");
4165
- }
4166
- const manifest = head.manifest;
4167
- const expectedProtected = /* @__PURE__ */ new Map();
4168
- for (const file of [...manifest.secretFiles, ...manifest.quarantinedResources]) {
4169
- expectedProtected.set(file.path, file);
4170
- }
4171
- if (protectedResponse.files.length !== expectedProtected.size) {
4172
- throw new Error("Cloud protected files do not match the revision manifest.");
4173
- }
4174
- const protectedBytes = /* @__PURE__ */ new Map();
4175
- for (const file of protectedResponse.files) {
4176
- const expected = expectedProtected.get(file.path);
4177
- const bytes = Buffer.from(file.contentBase64, "base64");
4178
- const canonicalBase64 = bytes.toString("base64").replace(/=+$/, "");
4179
- if (canonicalBase64 !== file.contentBase64.replace(/=+$/, "") || !expected || expected.kind !== file.kind || bytes.byteLength !== expected.size || sha2562(bytes) !== expected.sha256) {
4180
- throw new Error(`Cloud protected file failed integrity verification: ${file.path}`);
4181
- }
4182
- protectedBytes.set(file.path, bytes);
4183
- }
4184
- const localTargets = /* @__PURE__ */ new Set([
4185
- ...manifest.files.map((file) => file.path),
4186
- ...scan.manifest.secretFiles.map((file) => file.path),
4187
- ...scan.manifest.quarantinedResources.map((file) => file.path),
4188
- ...protectedBytes.keys()
4189
- ]);
4190
- for (const path of [...localTargets].sort()) await preflightReplacement(root, path);
4191
- for (const [path, bytes] of protectedBytes) await writeProtected(checkout, path, bytes);
4192
- if (scan.manifest.git.detected) {
4193
- git2(root, ["fetch", bundlePath, "refs/heads/main"]);
4194
- git2(root, ["reset", "--hard", "FETCH_HEAD"]);
4195
- const cloudFiles = new Set(manifest.files.map((file) => file.path));
4196
- for (const file of scan.manifest.files) {
4197
- if (!cloudFiles.has(file.path)) {
4198
- await rm2(join6(root, ...file.path.split("/")), { force: true });
4199
- }
4200
- }
4201
- } else {
4202
- for (const file of scan.manifest.files) await rm2(join6(root, ...file.path.split("/")), { force: true });
4203
- for (const file of manifest.files) {
4204
- const target = await assertSafeParent(root, file.path);
4205
- await copyFile2(join6(checkout, ...file.path.split("/")), target);
4206
- await chmod2(target, file.executable ? 493 : 420);
4207
- }
4208
- }
4209
- for (const old of [...scan.manifest.secretFiles, ...scan.manifest.quarantinedResources]) {
4210
- if (!protectedBytes.has(old.path)) await rm2(join6(root, ...old.path.split("/")), { force: true });
4211
- }
4212
- for (const [path, bytes] of protectedBytes) await writeProtected(root, path, bytes);
4213
- } finally {
4214
- await rm2(temporary, { recursive: true, force: true });
4215
- }
4216
- }
4217
-
4218
- // src/commands/sync-plan.ts
4219
- function describeDivergence(scan, state, cloud) {
4220
- const cloudFiles = new Map(manifestFiles(cloud.manifest).map((file) => [file.path, file.sha256]));
4221
- const localFiles = new Map(scan.manifest.files.map((file) => [file.path, file.sha256]));
4222
- let localOnly = 0;
4223
- let differing = 0;
4224
- for (const [path, sha] of localFiles) {
4225
- const remote = cloudFiles.get(path);
4226
- if (remote === void 0) localOnly += 1;
4227
- else if (remote !== sha) differing += 1;
4228
- }
4229
- let cloudOnly = 0;
4230
- for (const path of cloudFiles.keys()) if (!localFiles.has(path)) cloudOnly += 1;
4231
- return {
4232
- localChanged: !state.manifestSha256 || scan.manifestSha256 !== state.manifestSha256,
4233
- cloudChanged: cloud.id !== state.revisionId || cloud.generation !== state.generation,
4234
- local: { revisionId: state.revisionId, generation: state.generation },
4235
- cloud: { revisionId: cloud.id, generation: cloud.generation, commitSha: cloud.commitSha },
4236
- files: { local: localFiles.size, cloud: cloudFiles.size, localOnly, cloudOnly, differing }
4237
- };
4238
- }
4239
- function plannedAction(kind, divergence, force) {
4240
- const { localChanged, cloudChanged } = divergence;
4241
- if (kind === "push") return cloudChanged ? "conflict" : localChanged ? "push" : "none";
4242
- if (kind === "pull") {
4243
- if (!cloudChanged) return "none";
4244
- return localChanged && !force ? "conflict" : "pull";
4245
- }
4246
- if (localChanged && cloudChanged) return "conflict";
4247
- return localChanged ? "push" : cloudChanged ? "pull" : "none";
4248
- }
4249
- function requiresApproval(action, divergence, yes) {
4250
- return action === "pull" && divergence.localChanged && !yes;
4251
- }
4252
- var PLAN_SUMMARY = {
4253
- push: {
4254
- push: "Would push the local changes as a new cloud revision.",
4255
- pull: "",
4256
- none: "Nothing to push \u2014 local matches the last synced revision.",
4257
- conflict: "Would refuse: the cloud changed since this folder's base revision."
4258
- },
4259
- pull: {
4260
- push: "",
4261
- pull: "Would replace local files with the cloud revision.",
4262
- none: "Nothing to pull \u2014 the cloud head is the local base revision.",
4263
- conflict: "Would refuse: local files changed. Re-run with --force to replace them."
4264
- },
4265
- sync: {
4266
- push: "Would push the local changes as a new cloud revision.",
4267
- pull: "Would pull the cloud revision into this folder.",
4268
- none: "Already in sync.",
4269
- conflict: "Would refuse: local and cloud both changed since the last sync."
4270
- }
4271
- };
4272
- function renderPlan(kind, action, plan, approvalRequired) {
4273
- const files = plan.files;
4274
- return [
4275
- `${kind} --dry-run: ${PLAN_SUMMARY[kind][action]}`,
4276
- ...approvalRequired ? ["Requires --yes (or interactive approval) to execute."] : [],
4277
- "",
4278
- renderFields([
4279
- ["local", `generation ${plan.local.generation} (${plan.local.revisionId})${plan.localChanged ? " \u2014 modified" : ""}`],
4280
- ["cloud", `generation ${plan.cloud.generation} (${plan.cloud.revisionId})${plan.cloudChanged ? " \u2014 ahead" : ""}`],
4281
- ["files", `${files.local} local, ${files.cloud} cloud \u2014 ${files.localOnly} local-only, ${files.cloudOnly} cloud-only, ${files.differing} differing`]
4282
- ])
4283
- ].join("\n");
4284
- }
4285
-
4286
- // src/commands/sync.ts
4287
- function pushIdempotency(stateRevisionId, manifestSha256, bundleSha256) {
4288
- return createHash5("sha256").update(`project-push-v1\0${stateRevisionId}\0${manifestSha256}\0${bundleSha256}`).digest("hex");
4289
- }
4290
- async function binding(cwd) {
4291
- const found = findProjectBinding(cwd);
4292
- const state = found ? readProjectState(found.root) : null;
4293
- if (!found?.config.projectId || !state) {
4294
- throw new CliError("not_initialized", "This folder is not bound to an imported Hlix project. Run `hlix import .` first.");
4295
- }
4296
- return { root: await realpath5(found.root), config: found.config, state };
4297
- }
4298
- async function currentScan(root, environment, historyMode, rootName) {
4299
- const scan = await scanProject(root, { historyMode, environment, rootName });
4300
- if (scan.blocking.length > 0) {
4301
- throw new CliError("scan_blocked", `Project scan blocked sync:
4302
- - ${scan.blocking.join("\n- ")}`);
4303
- }
4304
- return scan;
4305
- }
4306
- async function dryRun(ctx, command, kind, root) {
4307
- const { state, config } = await binding(root);
4308
- const client = ctx.requireClient();
4309
- const [scan, headResponse] = await Promise.all([
4310
- currentScan(root, ctx.env, state.historyMode ?? "preserve", state.rootName),
4311
- client.revisions.head(config.projectId)
4312
- ]);
4313
- const plan = describeDivergence(scan, state, headResponse.revision);
4314
- const action = plannedAction(kind, plan, ctx.flags.force === true);
4315
- const approvalRequired = requiresApproval(action, plan, ctx.flags.yes === true);
4316
- if (ctx.json) {
4317
- ctx.stdout(renderJson(jsonSuccess(command, { dryRun: true, action, approvalRequired, ...plan })));
4318
- } else {
4319
- ctx.stdout(`${renderPlan(kind, action, plan, approvalRequired)}
4320
- `);
4321
- }
4322
- return action === "conflict" ? 1 : 0;
4323
- }
4324
- async function confirmForcedReplace(ctx, plan) {
4325
- if (ctx.flags.yes === true) return;
4326
- if (ctx.json || !ctx.confirm) {
4327
- throw new CliError(
4328
- "approval_required",
4329
- "`pull --force` replaces local files. Review with `hlix pull --dry-run`, then repeat with `--yes`."
4330
- );
4331
- }
4332
- const prompt = [
4333
- `hlix pull --force will replace this folder with cloud revision ${plan.cloud.generation}.`,
4334
- `Local-only files to delete: ${plan.files.localOnly}`,
4335
- `Files to replace with the cloud copy: ${plan.files.differing}`,
4336
- "Local changes that were never pushed cannot be recovered by Hlix."
4337
- ].join("\n");
4338
- if (!await ctx.confirm(prompt)) throw new CliError("cancelled", "Pull cancelled.");
4339
- }
4340
- async function pushRevision(root, ctx, scan, head) {
4341
- const { state, config } = await binding(root);
4342
- const client = ctx.requireClient();
4343
- const historyMode = state.historyMode ?? "preserve";
4344
- const current = scan ?? await currentScan(root, ctx.env, historyMode, state.rootName);
4345
- const cloud = head ?? (await client.revisions.head(config.projectId)).revision;
4346
- if (cloud.id !== state.revisionId || cloud.generation !== state.generation) {
4347
- throw new CliError("conflict", "Cloud changed since the local base. Run `hlix pull` or resolve with `hlix sync`.");
4348
- }
4349
- if (state.manifestSha256 && current.manifestSha256 === state.manifestSha256) {
4350
- return { action: "none", revisionId: state.revisionId, generation: state.generation };
4351
- }
4352
- const snapshot = await createProjectSnapshot(current);
4353
- try {
4354
- const created = await client.revisions.createUpload(config.projectId, {
4355
- idempotencyKey: pushIdempotency(state.revisionId, current.manifestSha256, snapshot.bundleSha256),
4356
- expectedRevisionId: state.revisionId,
4357
- expectedGeneration: state.generation,
4358
- manifest: current.manifest,
4359
- manifestSha256: current.manifestSha256,
4360
- bundleSha256: snapshot.bundleSha256,
4361
- bundleSize: snapshot.bundleSize,
4362
- commitSha: snapshot.commitSha
4363
- });
4364
- if (created.revisionUpload.status === "pending_upload") {
4365
- if (!created.upload) throw new Error("Revision upload has no bundle target.");
4366
- await client.revisions.uploadBundle(
4367
- created.upload,
4368
- new Blob([Uint8Array.from(await readFile4(snapshot.bundlePath))]),
4369
- {
4370
- onProgress: (uploaded, total) => {
4371
- if (!ctx.json) ctx.stderr(`Uploading ${uploaded}/${total} bytes\r`);
4372
- }
4373
- }
4374
- );
4375
- }
4376
- const finalized = await client.revisions.finalize(config.projectId, created.revisionUpload.id, {
4377
- secrets: current.secretValues,
4378
- protectedFiles: current.protectedFiles
4379
- });
4380
- if (!finalized.revision) throw new Error("Revision finalized without head metadata.");
4381
- writeProjectState(root, {
4382
- schemaVersion: 1,
4383
- revisionId: finalized.revision.id,
4384
- generation: finalized.revision.generation,
4385
- commitSha: finalized.revision.commitSha,
4386
- manifestSha256: finalized.revision.manifestSha256,
4387
- historyMode,
4388
- rootName: state.rootName ?? current.manifest.rootName
4389
- });
4390
- return { action: "push", ...finalized.revision };
4391
- } finally {
4392
- await snapshot.dispose();
4393
- }
4394
- }
4395
- async function pullRevision(root, ctx, force, scan, head) {
4396
- const { state, config } = await binding(root);
4397
- const client = ctx.requireClient();
4398
- const historyMode = state.historyMode ?? "preserve";
4399
- const current = scan ?? await currentScan(root, ctx.env, historyMode, state.rootName);
4400
- const cloud = head ?? (await client.revisions.head(config.projectId)).revision;
4401
- if (cloud.id === state.revisionId && cloud.generation === state.generation) {
4402
- return { action: "none", revisionId: state.revisionId, generation: state.generation };
4403
- }
4404
- const localChanged = !state.manifestSha256 || current.manifestSha256 !== state.manifestSha256;
4405
- if (localChanged && !force) {
4406
- throw new CliError("conflict", "Local files changed since the last sync. Push them, or use `hlix pull --force` to replace them.");
4407
- }
4408
- if (localChanged) await confirmForcedReplace(ctx, describeDivergence(current, state, cloud));
4409
- const [bundle, protectedFiles] = await Promise.all([
4410
- client.revisions.downloadBundle(config.projectId, cloud.id),
4411
- client.revisions.protectedFiles(config.projectId)
4412
- ]);
4413
- await applyPull(root, current, cloud, bundle, protectedFiles);
4414
- const cloudIdentity = manifestIdentity(cloud.manifest);
4415
- const pulledHistoryMode = cloudIdentity.historyMode ?? historyMode;
4416
- const pulledRootName = cloudIdentity.rootName ?? state.rootName ?? current.manifest.rootName;
4417
- const pulled = await currentScan(root, ctx.env, pulledHistoryMode, pulledRootName);
4418
- writeProjectState(root, {
4419
- schemaVersion: 1,
4420
- revisionId: cloud.id,
4421
- generation: cloud.generation,
4422
- commitSha: cloud.commitSha,
4423
- manifestSha256: pulled.manifestSha256,
4424
- historyMode: pulledHistoryMode,
4425
- rootName: pulledRootName
4426
- });
4427
- return { action: "pull", revisionId: cloud.id, generation: cloud.generation, commitSha: cloud.commitSha };
4428
- }
4429
- function output(ctx, command, result) {
4430
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(command, result)));
4431
- else if ("action" in result && result.action === "none") ctx.stdout("Already in sync.\n");
4432
- else ctx.stdout(`${String("action" in result ? result.action : "sync")} complete.
4433
- `);
4434
- }
4435
- var pushProject = {
4436
- name: "push",
4437
- summary: "Push local changes as an immutable cloud revision",
4438
- usage: "Usage: hlix push [--dry-run] [--json]",
4439
- supportsJson: true,
4440
- options: ["dry-run"],
4441
- async run(ctx) {
4442
- if (ctx.args.length) throw new Error("push accepts no positional arguments.");
4443
- const { root } = await binding(ctx.cwd);
4444
- if (ctx.flags["dry-run"] === true) return dryRun(ctx, pushProject.name, "push", root);
4445
- output(ctx, pushProject.name, await pushRevision(root, ctx));
4446
- return 0;
4447
- }
4448
- };
4449
- var pullProject = {
4450
- name: "pull",
4451
- summary: "Pull the cloud head into the local project",
4452
- usage: [
4453
- "Usage: hlix pull [--force] [--yes] [--dry-run] [--json]",
4454
- "",
4455
- " --force Replace local changes with the cloud revision",
4456
- " --yes Skip the confirmation --force otherwise requires",
4457
- " --dry-run Report the divergence and change nothing"
4458
- ].join("\n"),
4459
- supportsJson: true,
4460
- options: ["force", "yes", "dry-run"],
4461
- async run(ctx) {
4462
- if (ctx.args.length) throw new Error("pull accepts no positional arguments.");
4463
- const { root } = await binding(ctx.cwd);
4464
- if (ctx.flags["dry-run"] === true) return dryRun(ctx, pullProject.name, "pull", root);
4465
- output(ctx, pullProject.name, await pullRevision(root, ctx, ctx.flags.force === true));
4466
- return 0;
4467
- }
4468
- };
4469
- var syncProject = {
4470
- name: "sync",
4471
- summary: "Reconcile local and cloud changes without guessing conflicts",
4472
- usage: "Usage: hlix sync [--dry-run] [--json]",
4473
- supportsJson: true,
4474
- options: ["dry-run"],
4475
- async run(ctx) {
4476
- if (ctx.args.length) throw new Error("sync accepts no positional arguments.");
4477
- const { root, state, config } = await binding(ctx.cwd);
4478
- if (ctx.flags["dry-run"] === true) return dryRun(ctx, syncProject.name, "sync", root);
4479
- const client = ctx.requireClient();
4480
- const [scan, headResponse] = await Promise.all([
4481
- currentScan(root, ctx.env, state.historyMode ?? "preserve", state.rootName),
4482
- client.revisions.head(config.projectId)
4483
- ]);
4484
- const cloud = headResponse.revision;
4485
- const plan = describeDivergence(scan, state, cloud);
4486
- if (plan.localChanged && plan.cloudChanged) {
4487
- throw new CliError("conflict", "Both local and cloud changed since the last sync. Pull or push explicitly after reviewing the conflict.");
4488
- }
4489
- const result = plan.localChanged ? await pushRevision(root, ctx, scan, cloud) : plan.cloudChanged ? await pullRevision(root, ctx, false, scan, cloud) : { action: "none", revisionId: state.revisionId, generation: state.generation };
4490
- output(ctx, syncProject.name, result);
4491
- return 0;
4492
- }
4493
- };
4494
-
4495
- // src/commands/resources.ts
4496
- import { lstat as lstat4, readFile as readFile5, realpath as realpath6 } from "fs/promises";
4497
- import { basename as basename3, dirname as dirname6, extname, join as join7, parse, resolve as resolve8 } from "path";
4498
- function flag2(ctx, name) {
4499
- const value = ctx.flags[name];
4500
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
4501
- }
4502
- function requireProjectBinding(cwd) {
4503
- const binding2 = findProjectBinding(cwd);
4504
- if (!binding2?.config.projectId) {
4505
- throw new CliError(
4506
- "not_initialized",
4507
- "This folder is not bound to an Hlix project. Run `hlix import .` first."
4508
- );
4509
- }
4510
- return { root: binding2.root, projectId: binding2.config.projectId };
4511
- }
4512
- async function regularFile(path, maxBytes) {
4513
- const entry = await lstat4(path);
4514
- if (entry.isSymbolicLink() || !entry.isFile()) {
4515
- throw new Error(`Refusing to import a non-regular file: ${path}`);
4516
- }
4517
- if (entry.size > maxBytes) throw new Error(`Import file exceeds ${maxBytes} bytes: ${path}`);
4518
- return { path: await realpath6(path), content: await readFile5(path, "utf8") };
4519
- }
4520
- function writeResult(ctx, command, result, human) {
4521
- if (ctx.json) ctx.stdout(renderJson(jsonSuccess(command, result)));
4522
- else ctx.stdout(`${human}
4523
- `);
4524
- }
4525
- var importSkill = {
4526
- name: "skill import",
4527
- summary: "Activate a local skill for the bound cloud project",
4528
- usage: "Usage: hlix skill import <SKILL.md|folder> [--name <slug>] [--json]",
4529
- supportsJson: true,
4530
- options: ["name"],
4531
- args: { min: 1, max: 1 },
4532
- async run(ctx) {
4533
- if (ctx.args.length !== 1) throw new Error("skill import requires one file or folder.");
4534
- const binding2 = requireProjectBinding(ctx.cwd);
4535
- const input = resolve8(ctx.cwd, ctx.args[0]);
4536
- const entry = await lstat4(input);
4537
- if (entry.isSymbolicLink()) throw new Error(`Refusing to import a symlink: ${input}`);
4538
- const file = entry.isDirectory() ? join7(input, "SKILL.md") : input;
4539
- const loaded = await regularFile(file, 2e5);
4540
- const inferred = entry.isDirectory() ? basename3(input) : basename3(file) === "SKILL.md" ? basename3(dirname6(file)) : parse(file).name;
4541
- const slug = flag2(ctx, "name") ?? inferred;
4542
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
4543
- throw new Error("Skill name must be a lowercase kebab-case slug.");
4544
- }
4545
- const result = await ctx.requireClient().resources.importSkill(binding2.projectId, {
4546
- slug,
4547
- content: loaded.content
4548
- });
4549
- writeResult(ctx, importSkill.name, result.skill, `Imported skill ${slug}`);
4550
- return 0;
4551
- }
4552
- };
4553
- var importAgent = {
4554
- name: "agent import",
4555
- summary: "Create a project-scoped agent from a local Markdown brief",
4556
- usage: "Usage: hlix agent import <markdown-file> [--name <name>] [--runtime <runtime>] [--profile <profile>] [--json]",
4557
- supportsJson: true,
4558
- options: ["name", "runtime", "profile"],
4559
- args: { min: 1, max: 1 },
4560
- async run(ctx) {
4561
- if (ctx.args.length !== 1) throw new Error("agent import requires one Markdown file.");
4562
- const binding2 = requireProjectBinding(ctx.cwd);
4563
- const file = resolve8(ctx.cwd, ctx.args[0]);
4564
- if (![".md", ".mdx"].includes(extname(file).toLowerCase())) {
4565
- throw new Error("Agent imports must be Markdown files.");
4566
- }
4567
- const loaded = await regularFile(file, 2e4);
4568
- const runtime = flag2(ctx, "runtime") ?? "hlix";
4569
- if (!["claude-code", "codex", "cursor-agent", "hlix"].includes(runtime)) {
4570
- throw new Error("--runtime must be claude-code, codex, cursor-agent, or hlix.");
4571
- }
4572
- const profile = flag2(ctx, "profile") ?? "builder";
4573
- if (!["builder", "desktop", "e2e"].includes(profile)) {
4574
- throw new Error("--profile must be builder, desktop, or e2e.");
4575
- }
4576
- if (runtime === "hlix" && profile !== "builder") {
4577
- throw new Error(
4578
- `--profile ${profile} needs a sandbox runtime: pass --runtime claude-code (hlix ignores sandbox profiles).`
4579
- );
4580
- }
4581
- const name = flag2(ctx, "name") ?? parse(file).name.replace(/[-_]+/g, " ");
4582
- const result = await ctx.requireClient().resources.importAgent(binding2.projectId, {
4583
- name,
4584
- instructions: loaded.content,
4585
- runtimeKind: runtime,
4586
- sandboxProfile: profile
4587
- });
4588
- writeResult(ctx, importAgent.name, result.agent, `Imported agent ${result.agent.name}`);
4589
- return 0;
4590
- }
4591
- };
4592
- var importMcp = {
4593
- name: "mcp import",
4594
- summary: "Explicitly activate one validated local MCP server",
4595
- usage: "Usage: hlix mcp import <.mcp.json> [--server <name>] [--allow-stdio] [--json]",
4596
- supportsJson: true,
4597
- options: ["server", "allow-stdio"],
4598
- args: { min: 1, max: 1 },
4599
- async run(ctx) {
4600
- if (ctx.args.length !== 1) throw new Error("mcp import requires one MCP JSON file.");
4601
- const binding2 = requireProjectBinding(ctx.cwd);
4602
- const loaded = await regularFile(resolve8(ctx.cwd, ctx.args[0]), 2e5);
4603
- let parsed;
4604
- try {
4605
- parsed = JSON.parse(loaded.content);
4606
- } catch {
4607
- throw new Error("MCP config is not valid JSON.");
4608
- }
4609
- const servers = Object.entries(parsed.mcpServers ?? {});
4610
- if (servers.length === 0) throw new Error("MCP config contains no servers.");
4611
- const selectedName = flag2(ctx, "server");
4612
- if (!selectedName && servers.length > 1) {
4613
- throw new Error("MCP config contains multiple servers; select one with --server <name>.");
4614
- }
4615
- const selected = selectedName ? servers.find(([name2]) => name2 === selectedName) : servers[0];
4616
- if (!selected) throw new Error(`MCP server not found: ${selectedName}`);
4617
- const [name, raw] = selected;
4618
- const transport = raw.type === "http" || raw.type === "sse" ? raw.type : typeof raw.command === "string" ? "stdio" : typeof raw.url === "string" ? "http" : null;
4619
- if (!transport) throw new Error(`MCP server ${name} has no supported transport.`);
4620
- if (transport === "stdio" && ctx.flags["allow-stdio"] !== true) {
4621
- throw new Error("Stdio MCP servers execute a local command in the cloud; repeat with --allow-stdio.");
4622
- }
4623
- const env = raw.env && typeof raw.env === "object" && !Array.isArray(raw.env) ? raw.env : {};
4624
- const headers = raw.headers && typeof raw.headers === "object" && !Array.isArray(raw.headers) ? raw.headers : {};
4625
- if (Object.values(headers).some((value) => typeof value !== "string")) {
4626
- throw new Error(`MCP server ${name} headers must be strings.`);
4627
- }
4628
- const headerKeys = Object.values(headers).flatMap(
4629
- (value) => [...value.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map(
4630
- (match2) => match2[1]
4631
- )
4632
- );
4633
- if (Object.keys(headers).length > 0 && headerKeys.length === 0) {
4634
- throw new Error("MCP remote headers must use ${KEY} placeholders; literal credentials are refused.");
4635
- }
4636
- const validHeaderTemplate = /^(?:(?:Bearer|Basic|Token) )?\$\{[A-Za-z_][A-Za-z0-9_]*\}$/i;
4637
- if (Object.values(headers).some((value) => !validHeaderTemplate.test(value))) {
4638
- throw new Error("MCP remote headers must be ${KEY} or an approved auth scheme followed by ${KEY}.");
4639
- }
4640
- const result = await ctx.requireClient().resources.importMcp(binding2.projectId, {
4641
- name,
4642
- transport,
4643
- ...typeof raw.command === "string" ? { command: raw.command } : {},
4644
- ...Array.isArray(raw.args) && raw.args.every((arg) => typeof arg === "string") ? { args: raw.args } : {},
4645
- ...typeof raw.url === "string" ? { url: raw.url } : {},
4646
- ...Object.keys(headers).length > 0 ? { headers } : {},
4647
- envKeys: [.../* @__PURE__ */ new Set([...Object.keys(env), ...headerKeys])].sort()
4648
- });
4649
- writeResult(ctx, importMcp.name, result.mcpServer, `Imported MCP server ${name}`);
4650
- return 0;
4651
- }
4652
- };
4653
-
4654
- // src/index.ts
4655
- import { realpathSync } from "fs";
4656
- import { fileURLToPath } from "url";
4657
- var COMMANDS = [
4658
- authLogin,
4659
- authStatus,
4660
- authLogout,
4661
- statusCommand,
4662
- initProject,
4663
- importProject,
4664
- syncProject,
4665
- pullProject,
4666
- pushProject,
4667
- importSkill,
4668
- importAgent,
4669
- importMcp,
4670
- projectsList,
4671
- projectsGet,
4672
- tasksList,
4673
- tasksGet,
4674
- tasksReview,
4675
- tasksWatch
4676
- ];
4677
- function isMainModule() {
4678
- if (import.meta.main === true) return true;
4679
- const entry = process.argv[1];
4680
- if (!entry) return false;
4681
- try {
4682
- return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
4683
- } catch {
4684
- return false;
4685
- }
4686
- }
4687
- if (isMainModule()) {
4688
- const code = await runCli(COMMANDS, process.argv.slice(2), {
4689
- stdout: (text3) => process.stdout.write(text3),
4690
- stderr: (text3) => process.stderr.write(text3),
4691
- env: process.env,
4692
- readSecret: promptSecret,
4693
- confirm: promptConfirm
4694
- });
4695
- process.exit(code);
4696
- }
4697
- export {
4698
- COMMANDS,
4699
- JSON_SCHEMA_VERSION,
4700
- runCli
4701
- };
2
+ import{b as g,c as y,e as v,f as c,k as o}from"./index-619ayaee.js";import{l as i}from"./index-vfqh17y6.js";import{emitKeypressEvents as w}from"node:readline";import{createInterface as C}from"node:readline/promises";async function u(r){if(!process.stdin.isTTY||!process.stdin.setRawMode)throw Error("Interactive login requires a terminal. Set HLIX_API_KEY for CI.");return process.stderr.write(r),w(process.stdin),process.stdin.setRawMode(!0),process.stdin.resume(),new Promise((e,t)=>{let s="",p=()=>{process.stdin.off("keypress",a),process.stdin.setRawMode(!1),process.stdin.pause(),process.stderr.write(`
3
+ `)},a=(m,n)=>{if(n.ctrl&&n.name==="c"||n.name==="escape"){p(),t(Error("Login cancelled."));return}if(n.name==="return"||n.name==="enter"){p(),e(s);return}if(n.name==="backspace"){s=s.slice(0,-1);return}if(m&&!n.ctrl)s+=m};process.stdin.on("keypress",a)})}async function f(r){if(!process.stdin.isTTY||!process.stderr.isTTY)throw Error("Interactive approval requires a terminal. Review with --dry-run, then pass --yes.");let e=C({input:process.stdin,output:process.stderr});try{let t=(await e.question(`${r}
4
+ Continue? [y/N] `)).trim().toLowerCase();return t==="y"||t==="yes"}finally{e.close()}}import{realpathSync as l}from"node:fs";import{fileURLToPath as x}from"node:url";function h(){if(i.main==i.module===!0)return!0;let r=process.argv[1];if(!r)return!1;try{return l(r)===l(x(import.meta.url))}catch{return!1}}var d={"-h":"--help","-v":"--version"};function M(r){return r.map((e)=>d[e]??e)}function S(r,e=o){let t=r[0];if(t===void 0)return!1;if(t in d||t==="--help"||t==="--version")return!0;return e.some((s)=>s.name.split(" ")[0]===t)}function P(r,e){if(S(r))return e.cli(M(r));return e.code(r[0]==="code"?r.slice(1):r)}if(h()){let r=await P(process.argv.slice(2),{cli:(e)=>c(o,e,{stdout:(t)=>process.stdout.write(t),stderr:(t)=>process.stderr.write(t),env:process.env,readSecret:u,confirm:f}),code:async(e)=>(await import("./main-ct6p55pt.js")).runHlixCode(e)});process.exit(r)}export{o as COMMANDS,g as JSON_SCHEMA_VERSION,v as clientFor,P as dispatch,y as findProjectBinding,S as isCliInvocation,M as normalizeCliArgv,c as runCli};