deepline 0.3.62 → 0.3.64

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.
@@ -47,6 +47,8 @@ import { ConfigError } from './errors.js';
47
47
 
48
48
  export const HOST_URL_ENV = 'DEEPLINE_HOST_URL';
49
49
  export const API_KEY_ENV = 'DEEPLINE_API_KEY';
50
+ export const ACTIVE_ORG_ID_ENV = 'DEEPLINE_ACTIVE_ORG_ID';
51
+ export const ACTIVE_ORG_NAME_ENV = 'DEEPLINE_ACTIVE_ORG_NAME';
50
52
 
51
53
  /** Production API base URL. */
52
54
  const PROD_URL = 'https://code.deepline.com';
@@ -76,6 +78,21 @@ const COWORK_PROJECT_MARKERS = [
76
78
  ];
77
79
 
78
80
  type EnvValues = Record<string, string>;
81
+ const CLI_ENV_ALLOWED_KEYS = new Set([
82
+ HOST_URL_ENV,
83
+ API_KEY_ENV,
84
+ ACTIVE_ORG_ID_ENV,
85
+ ACTIVE_ORG_NAME_ENV,
86
+ ]);
87
+
88
+ export type CliCommandContext = {
89
+ active_organization: {
90
+ org_id: string;
91
+ org_name: string | null;
92
+ } | null;
93
+ auth_scope: 'env' | 'folder' | 'global' | null;
94
+ metadata_source: 'process_env' | 'folder_env' | 'host_env' | null;
95
+ };
79
96
  type ProjectEnvCandidate = {
80
97
  filePath: string;
81
98
  env: EnvValues;
@@ -155,10 +172,16 @@ function parseEnvFile(filePath: string): EnvValues {
155
172
  if (eqIndex < 0) continue;
156
173
  const key = trimmed.slice(0, eqIndex).trim();
157
174
  let value = trimmed.slice(eqIndex + 1).trim();
158
- if (
175
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
176
+ try {
177
+ value = JSON.parse(value) as string;
178
+ } catch {
179
+ value = value.slice(1, -1);
180
+ }
181
+ } else if (
159
182
  value.length >= 2 &&
160
- ((value.startsWith('"') && value.endsWith('"')) ||
161
- (value.startsWith("'") && value.endsWith("'")))
183
+ value.startsWith("'") &&
184
+ value.endsWith("'")
162
185
  ) {
163
186
  value = value.slice(1, -1);
164
187
  }
@@ -362,6 +385,35 @@ function firstNonEmpty(...values: Array<string | undefined | null>): string {
362
385
  return '';
363
386
  }
364
387
 
388
+ function formatEnvFileValue(value: string): string {
389
+ return /^[a-zA-Z0-9_./:@%+=,-]+$/.test(value) ? value : JSON.stringify(value);
390
+ }
391
+
392
+ function mergePersistedEnvValues(
393
+ existing: EnvValues,
394
+ values: EnvValues,
395
+ ): EnvValues {
396
+ const merged = { ...existing, ...values };
397
+ const nextApiKey = values[API_KEY_ENV];
398
+ if (
399
+ nextApiKey !== undefined &&
400
+ nextApiKey !== existing[API_KEY_ENV] &&
401
+ values[ACTIVE_ORG_ID_ENV] === undefined
402
+ ) {
403
+ delete merged[ACTIVE_ORG_ID_ENV];
404
+ delete merged[ACTIVE_ORG_NAME_ENV];
405
+ }
406
+ return merged;
407
+ }
408
+
409
+ function persistedEnvLines(values: EnvValues): string[] {
410
+ return Object.entries(values)
411
+ .filter(
412
+ ([key, value]) => CLI_ENV_ALLOWED_KEYS.has(key) && value.trim() !== '',
413
+ )
414
+ .map(([key, value]) => `${key}=${formatEnvFileValue(value)}`);
415
+ }
416
+
365
417
  function sdkCliConfigDir(baseUrl: string): string {
366
418
  const home = process.env.HOME?.trim() || homedir();
367
419
  return join(home, '.local', 'deepline', baseUrlSlug(baseUrl || PROD_URL));
@@ -404,11 +456,8 @@ export function saveHostEnvValues(baseUrl: string, values: EnvValues): void {
404
456
  }
405
457
 
406
458
  const existing = parseEnvFile(filePath);
407
- const merged = { ...existing, ...values };
408
- const allowedKeys = new Set([HOST_URL_ENV, API_KEY_ENV]);
409
- const lines = Object.entries(merged)
410
- .filter(([key, value]) => allowedKeys.has(key) && value !== '')
411
- .map(([key, value]) => `${key}=${value}`);
459
+ const merged = mergePersistedEnvValues(existing, values);
460
+ const lines = persistedEnvLines(merged);
412
461
  writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
413
462
  }
414
463
 
@@ -520,14 +569,11 @@ export function resolveConfig(options?: DeeplineClientOptions): ResolvedConfig {
520
569
 
521
570
  function mergeProjectEnvFile(filePath: string, values: EnvValues): void {
522
571
  const existing = parseEnvFile(filePath);
523
- const merged = { ...existing, ...values };
572
+ const merged = mergePersistedEnvValues(existing, values);
524
573
  const dir = dirname(filePath);
525
574
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
526
575
  ensureProjectEnvIsIgnored(dir);
527
- const allowedKeys = new Set([HOST_URL_ENV, API_KEY_ENV]);
528
- const lines = Object.entries(merged)
529
- .filter(([key, value]) => allowedKeys.has(key) && value !== '')
530
- .map(([key, value]) => `${key}=${value}`);
576
+ const lines = persistedEnvLines(merged);
531
577
  writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
532
578
  }
533
579
 
@@ -766,6 +812,100 @@ export function resolveCliAuthProvenance(
766
812
  };
767
813
  }
768
814
 
815
+ function activeOrganizationFromEnv(
816
+ env: EnvValues | NodeJS.ProcessEnv,
817
+ ): CliCommandContext['active_organization'] {
818
+ const orgId = env[ACTIVE_ORG_ID_ENV]?.trim();
819
+ if (!orgId) return null;
820
+ const orgName = env[ACTIVE_ORG_NAME_ENV]?.trim() || null;
821
+ return { org_id: orgId, org_name: orgName };
822
+ }
823
+
824
+ /**
825
+ * Resolve display-only organization metadata from the exact credential source
826
+ * selected for this CLI process. The metadata never participates in routing.
827
+ */
828
+ export function resolveCliCommandContext(
829
+ inputConfig?: Pick<ResolvedConfig, 'baseUrl' | 'apiKey'>,
830
+ requestedScope?: NonNullable<CliCommandContext['auth_scope']>,
831
+ ): CliCommandContext {
832
+ let config = inputConfig;
833
+ if (!config) {
834
+ try {
835
+ config = resolveConfig();
836
+ } catch {
837
+ return {
838
+ active_organization: null,
839
+ auth_scope: null,
840
+ metadata_source: null,
841
+ };
842
+ }
843
+ }
844
+
845
+ const contextForScope = (
846
+ scope: NonNullable<CliCommandContext['auth_scope']>,
847
+ ): CliCommandContext | null => {
848
+ if (scope === 'env') {
849
+ const processApiKey = process.env[API_KEY_ENV]?.trim();
850
+ return processApiKey && processApiKey === config.apiKey
851
+ ? {
852
+ active_organization: activeOrganizationFromEnv(process.env),
853
+ auth_scope: 'env',
854
+ metadata_source: 'process_env',
855
+ }
856
+ : null;
857
+ }
858
+ if (scope === 'folder') {
859
+ const projectAuth = getResolvedProjectAuthSource(
860
+ config.baseUrl,
861
+ config.apiKey,
862
+ );
863
+ return projectAuth
864
+ ? {
865
+ active_organization: activeOrganizationFromEnv(projectAuth.env),
866
+ auth_scope: 'folder',
867
+ metadata_source: 'folder_env',
868
+ }
869
+ : null;
870
+ }
871
+ const hostEnv = loadCliEnv(config.baseUrl);
872
+ return (hostEnv[API_KEY_ENV] ?? '').trim() === config.apiKey
873
+ ? {
874
+ active_organization: activeOrganizationFromEnv(hostEnv),
875
+ auth_scope: 'global',
876
+ metadata_source: 'host_env',
877
+ }
878
+ : null;
879
+ };
880
+
881
+ // An explicit process key always wins credential resolution, even when a
882
+ // command accepts --auth-scope to select a saved fallback. Report the
883
+ // provenance of the key the command actually used.
884
+ const processContext = contextForScope('env');
885
+ if (processContext) return processContext;
886
+
887
+ if (requestedScope) {
888
+ return (
889
+ contextForScope(requestedScope) ?? {
890
+ active_organization: null,
891
+ auth_scope: requestedScope,
892
+ metadata_source: null,
893
+ }
894
+ );
895
+ }
896
+
897
+ for (const scope of ['folder', 'global'] as const) {
898
+ const context = contextForScope(scope);
899
+ if (context) return context;
900
+ }
901
+
902
+ return {
903
+ active_organization: null,
904
+ auth_scope: null,
905
+ metadata_source: null,
906
+ };
907
+ }
908
+
769
909
  export {
770
910
  baseUrlSlug,
771
911
  loadCliEnv,
@@ -199,7 +199,7 @@ export const SDK_RELEASE = {
199
199
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
- version: '0.3.62',
202
+ version: '0.3.64',
203
203
  updateSummary:
204
204
  'Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.',
205
205
  packageCapabilities: {
@@ -1941,7 +1941,15 @@ async function analyzeSourceGraph(
1941
1941
  ): Promise<SourceGraphAnalysis> {
1942
1942
  const absoluteEntryFile = await normalizeLocalPath(entryFile);
1943
1943
  const workspace = createPlayWorkspace(absoluteEntryFile);
1944
- const sourceIdentityRoot = adapter.sourceIdentityRoot;
1944
+ // Normalize the identity root exactly as its files are normalized. Every
1945
+ // path it is compared against has been through realpath, so a root that has
1946
+ // not — a build under a symlinked temp dir, say — makes every relative()
1947
+ // escape with `..`, and sourceIdentityPath falls back to the absolute path.
1948
+ // The build directory's name then lands in graphHash, so two byte-identical
1949
+ // builds hash differently and every play re-versions on every deploy.
1950
+ const sourceIdentityRoot = adapter.sourceIdentityRoot
1951
+ ? await normalizeLocalPath(adapter.sourceIdentityRoot)
1952
+ : undefined;
1945
1953
  const localFiles = new Map<string, string>();
1946
1954
  const nodeBuiltins = new Set<string>();
1947
1955
  const packages = new Map<string, string | null>();