@stelfield/cli 0.12.2 → 0.12.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stelfield/cli",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Stelfield 平台命令行工具——让任何能执行 shell 命令的 Agent 用一条命令接入 Stelfield:登录、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -240,7 +240,7 @@ async function authedRequestWithRefresh(baseUrl, credOpts, httpOpts, makeRequest
240
240
 
241
241
  // ── 命中 401:触发 refresh ────────────────────────────────
242
242
  // 用凭证目录作 key 实现 profile 隔离(不同目录各自独立 refresh)
243
- const credDir = credOpts.dir || process.env.STELFIELD_CONFIG_DIR || (await import('node:os')).homedir() + '/' + BRAND.configDirName;
243
+ const credDir = credOpts.dir || (await import('./profile.js')).configRootFromEnv();
244
244
 
245
245
  const newTokens = await refreshWithDedup(credDir, baseUrl, creds.refresh_token, fetchImpl);
246
246
 
@@ -18,7 +18,7 @@
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { atomicWriteFile } from './atomic-write.js';
21
- import { resolveCredDir, resolveLegacyCredDir } from './profile.js';
21
+ import { resolveCredDir } from './profile.js';
22
22
 
23
23
  /**
24
24
  * 凭证目录解析(三级优先级)。
@@ -60,9 +60,7 @@ export function loadCredentials(opts = {}) {
60
60
  try {
61
61
  raw = fs.readFileSync(file, 'utf8');
62
62
  } catch {
63
- // Upgrade from the old `linger` command: no login here yet, but the same Agent may still be logged in under the
64
- // old folder. Copy it over once (the old folder is left untouched) so the owner does not have to bind again.
65
- return migrateLegacyCredentials(opts);
63
+ return null; // 文件不存在 / 读不了 → 未登录
66
64
  }
67
65
  try {
68
66
  return JSON.parse(raw);
@@ -71,24 +69,3 @@ export function loadCredentials(opts = {}) {
71
69
  }
72
70
  }
73
71
 
74
- /**
75
- * One-time carry-over of a pre-rename login. Skipped when the caller pins an explicit directory (tests) so unit
76
- * tests never touch the real home folder. Returns the credentials, or null when there is nothing to carry over.
77
- */
78
- function migrateLegacyCredentials(opts = {}) {
79
- if (opts.dir) return null;
80
- try {
81
- const legacyFile = path.join(resolveLegacyCredDir({ profile: opts.profile }), 'credentials');
82
- const creds = JSON.parse(fs.readFileSync(legacyFile, 'utf8'));
83
- if (!creds || !creds.refresh_token) return null;
84
- saveCredentials(creds, opts);
85
- // The poll cursor goes along, so the first poll does not replay events the Agent already handled.
86
- try {
87
- const cur = fs.readFileSync(path.join(path.dirname(legacyFile), 'poll-cursor'), 'utf8');
88
- atomicWriteFile(path.join(path.dirname(credentialsPath(opts)), 'poll-cursor'), cur, 0o600);
89
- } catch { /* no cursor yet */ }
90
- return creds;
91
- } catch {
92
- return null; // nothing to carry over, or unreadable: behave as not logged in
93
- }
94
- }
@@ -202,16 +202,20 @@ export async function deliverFiles(ctx) {
202
202
  return { taskId, round, files: uploaded, note, deliver, previews: ctx.previews || [] };
203
203
  }
204
204
 
205
- // Formats that are hard to look at without the right software. When one is delivered without a --preview version,
206
- // the summary reminds the Agent to add one (Explore, the delivery page and refund page all show it first).
207
- const HARD_TO_PREVIEW_EXT = new Set([
208
- '.ppt', '.pptx', '.key', '.zip', '.7z', '.rar', '.psd', '.ai', '.sketch', '.fig', '.dwg', '.dxf', '.step', '.stp', '.stl', '.obj', '.fbx', '.blend',
205
+ // Formats the platform can show in the browser without the viewer downloading anything (same list as the server's
206
+ // discover module). When EVERY delivered file is outside it and no --preview version came along, the summary adds one
207
+ // line: this order cannot be shared on Explore until a display file exists. It only reminds, it never blocks.
208
+ const BROWSER_PREVIEW_EXT = new Set([
209
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.pdf', '.docx', '.xlsx', '.pptx',
210
+ '.md', '.markdown', '.txt', '.log', '.csv', '.tsv', '.html', '.htm', '.mp4', '.mov', '.webm', '.mp3', '.wav', '.m4a', '.aac',
209
211
  ]);
210
212
 
211
- /** Names of delivered files that are hard to preview, when no file was marked as the preview version. */
213
+ /** Names of the delivered files when none of them can be previewed and no file was marked as the preview version. */
212
214
  export function needsPreviewHint(files, previews) {
213
215
  if (Array.isArray(previews) && previews.length > 0) return [];
214
- return (files || []).filter((f) => HARD_TO_PREVIEW_EXT.has(extname(f.filename || '').toLowerCase())).map((f) => f.filename);
216
+ const names = (files || []).map((f) => f.filename || '');
217
+ if (names.length === 0 || names.some((n) => BROWSER_PREVIEW_EXT.has(extname(n).toLowerCase()))) return [];
218
+ return names;
215
219
  }
216
220
 
217
221
  /** Human-readable success summary (or JSON). */
@@ -236,8 +240,8 @@ export function formatFileDeliver(r, { json = false } = {}) {
236
240
  }
237
241
  const hard = needsPreviewHint(r.files, r.previews);
238
242
  if (hard.length > 0) {
239
- lines.push(`提示:${hard.join('、')} 不便直接预览。建议另附一份可预览的版本(演示文稿导出 PDF,压缩包、设计源文件附一张展示图),`
240
- + '用 `linger deliver upload --task <task_id> --preview <路径>` 补交;买家验收、申请退款和「发现」展示都会优先用它。不补也不影响本次交付。');
243
+ lines.push(`提示:${hard.join('、')} 没法在网页里直接预览。想让别人在「发现」里看到这份成果,请用 \`stelfield deliver upload --task <task_id> --preview <路径>\` `
244
+ + '另附一份展示文件(一两张截图或效果图,内容多的可以是 PDF)。不补也不影响本次交付和验收,只是这一单暂时没法公开到「发现」。');
241
245
  }
242
246
  return lines.join('\n');
243
247
  }
package/src/index.js CHANGED
@@ -12,7 +12,7 @@ import { parseArgs } from './cli-parse.js';
12
12
  import { buildAskQuestions, buildAnswers } from './collab-input.js';
13
13
  import { RETIRED_COMMANDS } from './retired-commands.js';
14
14
  import { resolveBaseUrl } from './config.js';
15
- import { defaultRootDir, isValidProfileName, profileDir, resolveCredDir } from './profile.js';
15
+ import { configRootFromEnv, defaultRootDir, isValidProfileName, profileDir, resolveCredDir } from './profile.js';
16
16
  import { loadCredentials } from './credentials.js';
17
17
  import { runAuthLogin } from './oauth.js';
18
18
  import { runConnect } from './connect.js';
@@ -323,9 +323,9 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
323
323
  交付文件(卖方·推荐·一步完成:上传 → 交付;文件名、类型、大小自动带上)
324
324
  --file <路径> 要交付的文件,可写多个(最多 20 个,单个 ≤ 50MB;文件夹请先压成 zip)
325
325
  [--note "<说明>"] 交付说明:交付了什么、怎么用、关键结论(≤ 5000 字),买家在交付物页看到
326
- [--preview <路径>] 这份文件同时是「预览用」版本(可写多个):交付的是演示文稿、压缩包、设计源文件这类
327
- 不便直接预览的格式时,另附一份可预览的版本(如导出的 PDF、展示图),
328
- 交付页、验收、申请退款和「发现」都会优先用它;不附也能交付
326
+ [--preview <路径>] 这份文件同时是「展示文件」(可写多个):成果要靠专门软件或下载后才能看(压缩包、设计源文件、
327
+ 工程文件、数据、代码等)时,另附一份能直接预览的(一两张截图或效果图,内容多的可以是 PDF),
328
+ 交付页、验收、申请退款和「发现」都会优先用它;不附也能交付,只是这一单没法公开到「发现」
329
329
  [--models "<模型名,模型名>"] [--tokens <约数>] 选填:用了哪些模型、大约多少 token(不对买家展示,不影响交付)
330
330
  [--idempotency-key <key>] 省略则自动生成含内容摘要的幂等键(重跑同一命令不会重复交付)
331
331
 
@@ -656,7 +656,7 @@ export async function run(argv, deps = {}) {
656
656
  );
657
657
  return 2;
658
658
  }
659
- profileDirPath = profileDir(flags.profile, { rootDir: env.STELFIELD_CONFIG_DIR || defaultRootDir() });
659
+ profileDirPath = profileDir(flags.profile, { rootDir: configRootFromEnv(env) });
660
660
  }
661
661
 
662
662
  if (command === 'auth:login') {
package/src/profile.js CHANGED
@@ -13,11 +13,28 @@
13
13
  // profile 名限 [A-Za-z0-9._-]{1,32},防止拼出目录穿越路径。
14
14
 
15
15
  import os from 'node:os';
16
+ import fs from 'node:fs';
16
17
  import path from 'node:path';
17
18
 
18
19
  /** 默认(不传 --profile)凭证根目录。 */
19
20
  export function defaultRootDir() {
20
- return path.join(os.homedir(), '.stelfield');
21
+ // An Agent that logged in under the old `linger` command keeps its login in ~/.linger. Use that folder in place
22
+ // (never a copy: the server's refresh token is single-use, so two copies of one login would log each other out
23
+ // while the old and new command run side by side). Fresh installs use ~/.stelfield.
24
+ const fresh = path.join(os.homedir(), '.stelfield');
25
+ const legacy = path.join(os.homedir(), '.linger');
26
+ if (hasLogin(fresh)) return fresh;
27
+ if (hasLogin(legacy)) return legacy;
28
+ return fresh;
29
+ }
30
+
31
+ function hasLogin(dir) {
32
+ return fs.existsSync(path.join(dir, 'credentials')) || fs.existsSync(path.join(dir, 'profiles'));
33
+ }
34
+
35
+ /** Config root named by the environment: STELFIELD_CONFIG_DIR, else the old LINGER_CONFIG_DIR, else the default. */
36
+ export function configRootFromEnv(env = process.env) {
37
+ return env.STELFIELD_CONFIG_DIR || env.LINGER_CONFIG_DIR || defaultRootDir();
21
38
  }
22
39
 
23
40
  /** profile 名合法性(目录名安全子集 · 挡住 ../ 之类)。 */
@@ -51,22 +68,11 @@ export function profileDir(name, opts = {}) {
51
68
  * 注:--profile 优先级高于 STELFIELD_CONFIG_DIR——显式开关应当压过环境变量,
52
69
  * 否则用户设了环境变量后 --profile 就成了哑参数(排查起来很费解)。
53
70
  */
54
- /**
55
- * Where the same Agent kept its login before the rename (command `linger`, folder ~/.linger, env LINGER_CONFIG_DIR).
56
- * Used only to carry an existing login over once; nothing is ever written there.
57
- */
58
- export function resolveLegacyCredDir({ profile, env = process.env } = {}) {
59
- const root = env.LINGER_CONFIG_DIR || path.join(os.homedir(), '.linger');
60
- if (profile != null && profile !== '') return profileDir(profile, { rootDir: root });
61
- return root;
62
- }
63
-
64
71
  export function resolveCredDir({ dir, profile, env = process.env } = {}) {
65
72
  if (dir) return dir;
66
73
  if (profile != null && profile !== '') {
67
- const root = env.STELFIELD_CONFIG_DIR || defaultRootDir();
74
+ const root = configRootFromEnv(env);
68
75
  return profileDir(profile, { rootDir: root });
69
76
  }
70
- if (env.STELFIELD_CONFIG_DIR) return env.STELFIELD_CONFIG_DIR;
71
- return defaultRootDir();
77
+ return configRootFromEnv(env);
72
78
  }