@ship-cli/core 0.1.10 → 0.1.12

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.
Files changed (2) hide show
  1. package/dist/bin.js +260 -40
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -39834,6 +39834,67 @@ Project: ${partial2.linear.value.projectId.value}` : ""}`,
39834
39834
  Se("Run 'ship task ready' to see available tasks.");
39835
39835
  return;
39836
39836
  }
39837
+ if (isSome2(partial2.auth) && isSome2(partial2.notion)) {
39838
+ Me(
39839
+ `Provider: Notion
39840
+ Database: ${partial2.notion.value.databaseId}`,
39841
+ "Already initialized"
39842
+ );
39843
+ Se("Run 'ship task ready' to see available tasks.");
39844
+ return;
39845
+ }
39846
+ }
39847
+ const provider = yield* prompts.select({
39848
+ message: "Select task provider",
39849
+ options: [
39850
+ { value: "linear", label: "Linear", hint: "recommended" },
39851
+ { value: "notion", label: "Notion", hint: "use Notion database as task backend" }
39852
+ ]
39853
+ });
39854
+ if (provider === "notion") {
39855
+ Me(
39856
+ "Create an integration at:\nhttps://www.notion.so/my-integrations\n\nThen share your task database with the integration.",
39857
+ "Notion Authentication"
39858
+ );
39859
+ const notionToken = yield* prompts.text({
39860
+ message: "Paste your Notion API token",
39861
+ placeholder: "ntn_... or secret_...",
39862
+ validate: (value5) => {
39863
+ if (!value5) return "API token is required";
39864
+ if (!value5.startsWith("ntn_") && !value5.startsWith("secret_"))
39865
+ return "Token should start with ntn_ or secret_";
39866
+ return void 0;
39867
+ }
39868
+ });
39869
+ const databaseId = yield* prompts.text({
39870
+ message: "Paste your Notion database ID",
39871
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
39872
+ validate: (value5) => {
39873
+ if (!value5) return "Database ID is required";
39874
+ const cleaned = value5.replace(/-/g, "");
39875
+ if (cleaned.length !== 32) return "Invalid database ID format";
39876
+ return void 0;
39877
+ }
39878
+ });
39879
+ yield* config2.saveAuth({ apiKey: notionToken });
39880
+ yield* config2.saveNotion(
39881
+ new NotionConfig({
39882
+ databaseId: databaseId.replace(/-/g, ""),
39883
+ workspaceId: none2(),
39884
+ propertyMapping: new NotionPropertyMapping({})
39885
+ })
39886
+ );
39887
+ yield* config2.ensureGitignore();
39888
+ yield* config2.ensureOpencodeSkill();
39889
+ Me(
39890
+ `Provider: Notion
39891
+ Database: ${databaseId}
39892
+
39893
+ OpenCode skill created at .opencode/skill/ship-cli/SKILL.md`,
39894
+ "Workspace initialized"
39895
+ );
39896
+ Se("Run 'ship task ready' to see available tasks.");
39897
+ return;
39837
39898
  }
39838
39899
  const isAuth = yield* auth.isAuthenticated();
39839
39900
  if (!isAuth) {
@@ -188844,6 +188905,28 @@ var restackCommand = make58(
188844
188905
  return;
188845
188906
  }
188846
188907
  const { vcs } = vcsCheck;
188908
+ const defaultBranch = yield* getDefaultBranch();
188909
+ if (!json2) {
188910
+ yield* log3("Fetching from remote...");
188911
+ }
188912
+ const fetchResult = yield* vcs.fetch().pipe(
188913
+ map17(() => ({ success: true })),
188914
+ catchAll2((e2) => succeed7({ success: false, error: formatEffectError(e2) }))
188915
+ );
188916
+ if (!fetchResult.success) {
188917
+ const output2 = {
188918
+ fetched: false,
188919
+ restacked: false,
188920
+ pushed: false,
188921
+ error: fetchResult.error
188922
+ };
188923
+ if (json2) {
188924
+ yield* log3(JSON.stringify(output2, null, 2));
188925
+ } else {
188926
+ yield* outputError(`Fetch failed: ${fetchResult.error}`, json2);
188927
+ }
188928
+ return;
188929
+ }
188847
188930
  const stack2 = yield* vcs.getStack().pipe(
188848
188931
  catchAll2((e2) => {
188849
188932
  return succeed7({ error: formatEffectError(e2), changes: [] });
@@ -188860,11 +188943,15 @@ var restackCommand = make58(
188860
188943
  catchAll2(() => succeed7({ success: false }))
188861
188944
  );
188862
188945
  const output2 = trunkResult2.success ? {
188946
+ fetched: true,
188863
188947
  restacked: false,
188948
+ pushed: false,
188864
188949
  stackSize: 0,
188865
188950
  trunkChangeId: trunkResult2.trunk.shortChangeId
188866
188951
  } : {
188952
+ fetched: true,
188867
188953
  restacked: false,
188954
+ pushed: false,
188868
188955
  stackSize: 0
188869
188956
  };
188870
188957
  if (json2) {
@@ -188877,8 +188964,10 @@ var restackCommand = make58(
188877
188964
  }
188878
188965
  return;
188879
188966
  }
188967
+ if (!json2) {
188968
+ yield* log3("Rebasing stack onto trunk...");
188969
+ }
188880
188970
  const firstInStack = changes2[changes2.length - 1];
188881
- const defaultBranch = yield* getDefaultBranch();
188882
188971
  const rebaseResult = yield* vcs.rebase(firstInStack.id, defaultBranch).pipe(
188883
188972
  map17(() => ({ success: true, conflicted: false })),
188884
188973
  catchTag2(
@@ -188891,7 +188980,9 @@ var restackCommand = make58(
188891
188980
  );
188892
188981
  if (!rebaseResult.success) {
188893
188982
  const output2 = {
188983
+ fetched: true,
188894
188984
  restacked: false,
188985
+ pushed: false,
188895
188986
  error: rebaseResult.error
188896
188987
  };
188897
188988
  if (json2) {
@@ -188901,43 +188992,96 @@ var restackCommand = make58(
188901
188992
  }
188902
188993
  return;
188903
188994
  }
188995
+ if (rebaseResult.conflicted) {
188996
+ const stackAfter2 = yield* vcs.getStack().pipe(
188997
+ map17((s) => s.length),
188998
+ catchAll2(() => succeed7(changes2.length))
188999
+ );
189000
+ const trunkResult2 = yield* vcs.getTrunkInfo().pipe(
189001
+ map17((trunk) => ({ success: true, trunk })),
189002
+ catchAll2(() => succeed7({ success: false }))
189003
+ );
189004
+ const output2 = {
189005
+ fetched: true,
189006
+ restacked: true,
189007
+ pushed: false,
189008
+ stackSize: stackAfter2,
189009
+ trunkChangeId: trunkResult2.success ? trunkResult2.trunk.shortChangeId : void 0,
189010
+ conflicted: true
189011
+ };
189012
+ if (json2) {
189013
+ yield* log3(JSON.stringify(output2, null, 2));
189014
+ } else {
189015
+ yield* log3("Restack completed with conflicts!");
189016
+ yield* log3(` Fetched: yes`);
189017
+ yield* log3(` Rebased: yes (with conflicts)`);
189018
+ if (trunkResult2.success) {
189019
+ yield* log3(` Trunk: ${trunkResult2.trunk.shortChangeId.slice(0, 12)}`);
189020
+ }
189021
+ yield* log3(` Stack: ${stackAfter2} change(s)`);
189022
+ yield* log3(` Pushed: no (conflicts must be resolved first)`);
189023
+ yield* log3("");
189024
+ yield* log3("Resolve conflicts with 'jj status' and edit the conflicted files.");
189025
+ yield* log3("Then run 'ship stack restack' again to push.");
189026
+ }
189027
+ return;
189028
+ }
189029
+ if (!json2) {
189030
+ yield* log3("Pushing bookmarks...");
189031
+ }
188904
189032
  const stackAfter = yield* vcs.getStack().pipe(
188905
- map17((s) => s.length),
188906
- catchAll2(() => succeed7(changes2.length))
189033
+ catchAll2(() => succeed7([]))
188907
189034
  );
189035
+ const bookmarksToPush = stackAfter.flatMap((c) => c.bookmarks).filter((b3) => b3 && b3.length > 0);
189036
+ const pushedBookmarks = [];
189037
+ for (const bookmark of bookmarksToPush) {
189038
+ const pushResult = yield* vcs.push(bookmark).pipe(
189039
+ map17(() => ({ success: true })),
189040
+ catchAll2((e2) => succeed7({ success: false, error: formatEffectError(e2) }))
189041
+ );
189042
+ pushedBookmarks.push({
189043
+ bookmark,
189044
+ success: pushResult.success,
189045
+ error: "error" in pushResult ? pushResult.error : void 0
189046
+ });
189047
+ }
189048
+ const allPushed = pushedBookmarks.every((p4) => p4.success);
189049
+ const anyPushed = pushedBookmarks.some((p4) => p4.success);
188908
189050
  const trunkResult = yield* vcs.getTrunkInfo().pipe(
188909
189051
  map17((trunk) => ({ success: true, trunk })),
188910
189052
  catchAll2(() => succeed7({ success: false }))
188911
189053
  );
188912
- const output = trunkResult.success ? {
188913
- restacked: true,
188914
- stackSize: stackAfter,
188915
- trunkChangeId: trunkResult.trunk.shortChangeId,
188916
- conflicted: rebaseResult.conflicted
188917
- } : {
189054
+ const output = {
189055
+ fetched: true,
188918
189056
  restacked: true,
188919
- stackSize: stackAfter,
188920
- conflicted: rebaseResult.conflicted
189057
+ pushed: anyPushed,
189058
+ stackSize: stackAfter.length,
189059
+ trunkChangeId: trunkResult.success ? trunkResult.trunk.shortChangeId : void 0,
189060
+ conflicted: false,
189061
+ pushedBookmarks: pushedBookmarks.length > 0 ? pushedBookmarks : void 0
188921
189062
  };
188922
189063
  if (json2) {
188923
189064
  yield* log3(JSON.stringify(output, null, 2));
188924
189065
  } else {
188925
- if (rebaseResult.conflicted) {
188926
- yield* log3("Restack completed with conflicts!");
188927
- yield* log3(` Rebased: yes (with conflicts)`);
188928
- if (trunkResult.success) {
188929
- yield* log3(` Trunk: ${trunkResult.trunk.shortChangeId.slice(0, 12)}`);
189066
+ yield* log3("Restack completed successfully.");
189067
+ yield* log3(` Fetched: yes`);
189068
+ yield* log3(` Rebased: ${changes2.length} change(s)`);
189069
+ if (trunkResult.success) {
189070
+ yield* log3(` Trunk: ${trunkResult.trunk.shortChangeId.slice(0, 12)}`);
189071
+ }
189072
+ yield* log3(` Stack: ${stackAfter.length} change(s)`);
189073
+ if (pushedBookmarks.length > 0) {
189074
+ const successCount = pushedBookmarks.filter((p4) => p4.success).length;
189075
+ yield* log3(` Pushed: ${successCount}/${pushedBookmarks.length} bookmark(s)`);
189076
+ if (!allPushed) {
189077
+ yield* log3("");
189078
+ yield* log3("Failed to push:");
189079
+ for (const p4 of pushedBookmarks.filter((p5) => !p5.success)) {
189080
+ yield* log3(` - ${p4.bookmark}: ${p4.error}`);
189081
+ }
188930
189082
  }
188931
- yield* log3(` Stack: ${stackAfter} change(s)`);
188932
- yield* log3("");
188933
- yield* log3("Resolve conflicts with 'jj status' and edit the conflicted files.");
188934
189083
  } else {
188935
- yield* log3("Restack completed successfully.");
188936
- yield* log3(` Rebased: ${changes2.length} change(s)`);
188937
- if (trunkResult.success) {
188938
- yield* log3(` Trunk: ${trunkResult.trunk.shortChangeId.slice(0, 12)}`);
188939
- }
188940
- yield* log3(` Stack: ${stackAfter} change(s)`);
189084
+ yield* log3(` Pushed: no bookmarks to push`);
188941
189085
  }
188942
189086
  }
188943
189087
  })
@@ -190079,7 +190223,7 @@ Commands:
190079
190223
  describe Update change description
190080
190224
  bookmark Create or move a bookmark on current change
190081
190225
  sync Fetch and rebase onto trunk
190082
- restack Rebase entire stack onto trunk (no fetch)
190226
+ restack Fetch, rebase, and push entire stack (like Graphite)
190083
190227
  submit Push and create/update PR
190084
190228
  squash Squash current change into parent
190085
190229
  abandon Abandon a change
@@ -190249,7 +190393,7 @@ Repository: ${repoName}
190249
190393
  Title: ${prTitle}
190250
190394
  URL: ${prUrl}
190251
190395
 
190252
- \u2192 Action: Run stack-sync --auto-submit to rebase and push dependent PRs`;
190396
+ \u2192 Action: Run stack-restack to rebase and push dependent PRs`;
190253
190397
  }
190254
190398
  return `[GitHub] PR #${prNumber} closed (not merged) by @${senderLogin}
190255
190399
 
@@ -192754,7 +192898,7 @@ var command = ship.pipe(
192754
192898
  prCommand
192755
192899
  ])
192756
192900
  );
192757
- var version = "0.1.10" ;
192901
+ var version = "0.1.12" ;
192758
192902
  var run9 = run8(command, {
192759
192903
  name: "ship",
192760
192904
  version
@@ -194443,7 +194587,7 @@ All support optional \`workdir\` param.
194443
194587
  | Action | Params | Description |
194444
194588
  |--------|--------|-------------|
194445
194589
  | \`stack-sync\` | - | Fetch + rebase onto trunk |
194446
- | \`stack-restack\` | - | Rebase onto trunk (no fetch) |
194590
+ | \`stack-restack\` | - | Fetch + rebase + push entire stack (like Graphite restack) |
194447
194591
  | \`stack-create\` | message?, bookmark?, noWorkspace? | New change (creates workspace by default) |
194448
194592
  | \`stack-describe\` | title, description? OR message | Update description (use title+description for proper multi-line commits) |
194449
194593
  | \`stack-submit\` | draft? | Push + create/update PR |
@@ -194503,6 +194647,7 @@ var CONFIG_FILE = "config.yaml";
194503
194647
  var OPENCODE_SKILL_DIR = ".opencode/skill/ship-cli";
194504
194648
  var OPENCODE_SKILL_FILE = "SKILL.md";
194505
194649
  var YamlConfig = Struct({
194650
+ provider: optional(Literal2("linear", "notion")),
194506
194651
  linear: optional(
194507
194652
  Struct({
194508
194653
  teamId: String$,
@@ -194510,6 +194655,25 @@ var YamlConfig = Struct({
194510
194655
  projectId: NullOr(String$)
194511
194656
  })
194512
194657
  ),
194658
+ notion: optional(
194659
+ Struct({
194660
+ databaseId: String$,
194661
+ workspaceId: NullOr(String$),
194662
+ propertyMapping: optional(
194663
+ Struct({
194664
+ title: optional(String$),
194665
+ status: optional(String$),
194666
+ priority: optional(String$),
194667
+ description: optional(String$),
194668
+ labels: optional(String$),
194669
+ blockedBy: optional(String$),
194670
+ type: optional(String$),
194671
+ identifier: optional(String$),
194672
+ parent: optional(String$)
194673
+ })
194674
+ )
194675
+ })
194676
+ ),
194513
194677
  auth: optional(
194514
194678
  Struct({
194515
194679
  apiKey: String$
@@ -194621,7 +194785,7 @@ Details: ${e2.message}`,
194621
194785
  });
194622
194786
  }
194623
194787
  return new PartialShipConfig({
194624
- // provider defaults to "linear" in schema
194788
+ provider: yaml.provider ?? "linear",
194625
194789
  linear: yaml.linear ? some2(
194626
194790
  new LinearConfig({
194627
194791
  teamId: asTeamId(yaml.linear.teamId),
@@ -194629,8 +194793,13 @@ Details: ${e2.message}`,
194629
194793
  projectId: yaml.linear.projectId ? some2(asProjectId(yaml.linear.projectId)) : none2()
194630
194794
  })
194631
194795
  ) : none2(),
194632
- notion: none2(),
194633
- // TODO: Parse notion config from yaml when implemented
194796
+ notion: yaml.notion ? some2(
194797
+ new NotionConfig({
194798
+ databaseId: yaml.notion.databaseId,
194799
+ workspaceId: yaml.notion.workspaceId ? some2(yaml.notion.workspaceId) : none2(),
194800
+ propertyMapping: new NotionPropertyMapping({})
194801
+ })
194802
+ ) : none2(),
194634
194803
  auth: yaml.auth ? some2(new AuthConfig({ apiKey: yaml.auth.apiKey })) : none2()
194635
194804
  });
194636
194805
  };
@@ -194671,17 +194840,37 @@ Details: ${e2.message}`,
194671
194840
  });
194672
194841
  const load = () => gen2(function* () {
194673
194842
  const yaml = yield* readYaml();
194674
- if (!yaml || !yaml.linear || !yaml.auth) {
194843
+ if (!yaml || !yaml.auth) {
194844
+ return yield* fail7(WorkspaceNotInitializedError.default);
194845
+ }
194846
+ const provider = yaml.provider ?? "linear";
194847
+ if (provider === "linear" && !yaml.linear) {
194675
194848
  return yield* fail7(WorkspaceNotInitializedError.default);
194676
194849
  }
194850
+ if (provider === "notion" && !yaml.notion) {
194851
+ return yield* fail7(WorkspaceNotInitializedError.default);
194852
+ }
194853
+ const linearConfig = yaml.linear ? new LinearConfig({
194854
+ teamId: asTeamId(yaml.linear.teamId),
194855
+ teamKey: yaml.linear.teamKey,
194856
+ projectId: yaml.linear.projectId ? some2(asProjectId(yaml.linear.projectId)) : none2()
194857
+ }) : new LinearConfig({
194858
+ teamId: asTeamId("notion-workspace"),
194859
+ teamKey: "NOTION",
194860
+ projectId: none2()
194861
+ });
194862
+ const notionConfig = yaml.notion ? some2(
194863
+ new NotionConfig({
194864
+ databaseId: yaml.notion.databaseId,
194865
+ workspaceId: yaml.notion.workspaceId ? some2(yaml.notion.workspaceId) : none2(),
194866
+ // Use defaults from NotionPropertyMapping, override with yaml values if present
194867
+ propertyMapping: new NotionPropertyMapping({})
194868
+ })
194869
+ ) : none2();
194677
194870
  return new ShipConfig({
194678
- linear: new LinearConfig({
194679
- teamId: asTeamId(yaml.linear.teamId),
194680
- teamKey: yaml.linear.teamKey,
194681
- projectId: yaml.linear.projectId ? some2(asProjectId(yaml.linear.projectId)) : none2()
194682
- }),
194683
- notion: none2(),
194684
- // TODO: Parse notion config from yaml when implemented
194871
+ provider,
194872
+ linear: linearConfig,
194873
+ notion: notionConfig,
194685
194874
  auth: new AuthConfig({ apiKey: yaml.auth.apiKey }),
194686
194875
  git: new GitConfig({ defaultBranch: yaml.git?.defaultBranch ?? "main" }),
194687
194876
  pr: new PrConfig({ openBrowser: yaml.pr?.openBrowser ?? true }),
@@ -194738,6 +194927,36 @@ Details: ${e2.message}`,
194738
194927
  };
194739
194928
  yield* writeYaml(yaml);
194740
194929
  });
194930
+ const saveNotion = (notion) => gen2(function* () {
194931
+ const existingYaml = yield* readYaml();
194932
+ const yaml = {};
194933
+ if (existingYaml?.auth) {
194934
+ yaml.auth = { apiKey: existingYaml.auth.apiKey };
194935
+ }
194936
+ if (existingYaml?.git?.defaultBranch)
194937
+ yaml.git = { defaultBranch: existingYaml.git.defaultBranch };
194938
+ if (existingYaml?.pr?.openBrowser !== void 0)
194939
+ yaml.pr = { openBrowser: existingYaml.pr.openBrowser };
194940
+ if (existingYaml?.commit?.conventionalFormat !== void 0)
194941
+ yaml.commit = { conventionalFormat: existingYaml.commit.conventionalFormat };
194942
+ yaml.provider = "notion";
194943
+ yaml.notion = {
194944
+ databaseId: notion.databaseId,
194945
+ workspaceId: isSome2(notion.workspaceId) ? notion.workspaceId.value : null,
194946
+ propertyMapping: {
194947
+ title: notion.propertyMapping.title,
194948
+ status: notion.propertyMapping.status,
194949
+ priority: notion.propertyMapping.priority,
194950
+ description: notion.propertyMapping.description,
194951
+ labels: notion.propertyMapping.labels,
194952
+ blockedBy: notion.propertyMapping.blockedBy,
194953
+ type: notion.propertyMapping.type,
194954
+ identifier: notion.propertyMapping.identifier,
194955
+ parent: notion.propertyMapping.parent
194956
+ }
194957
+ };
194958
+ yield* writeYaml(yaml);
194959
+ });
194741
194960
  const deleteConfig = () => gen2(function* () {
194742
194961
  const configPath = yield* getConfigPath();
194743
194962
  const fileExists = yield* fs.exists(configPath);
@@ -194772,6 +194991,7 @@ Details: ${e2.message}`,
194772
194991
  savePartial,
194773
194992
  saveAuth,
194774
194993
  saveLinear,
194994
+ saveNotion,
194775
194995
  exists: exists3,
194776
194996
  getConfigDir,
194777
194997
  ensureConfigDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-cli/core",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Linear + jj workflow CLI for AI agents",